Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: generic smtp provider for email notifications #18

Merged
merged 4 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

"github.com/vocdoni/saas-backend/account"
"github.com/vocdoni/saas-backend/db"
"github.com/vocdoni/saas-backend/notifications/testmail"
"github.com/vocdoni/saas-backend/notifications/smtp"
"github.com/vocdoni/saas-backend/test"
"go.vocdoni.io/dvote/apiclient"
)
Expand Down Expand Up @@ -44,7 +44,7 @@ var testDB *db.MongoStorage

// testMailService is the test mail service for the tests. Make it global so it
// can be accessed by the tests directly.
var testMailService *testmail.TestMail
var testMailService *smtp.SMTPEmail

// testURL helper function returns the full URL for the given path using the
// test host and port.
Expand Down Expand Up @@ -161,14 +161,14 @@ func TestMain(m *testing.M) {
panic(err)
}
// create test mail service
testMailService = new(testmail.TestMail)
if err := testMailService.Init(&testmail.TestMailConfig{
testMailService = new(smtp.SMTPEmail)
if err := testMailService.New(&smtp.SMTPConfig{
FromAddress: adminEmail,
SMTPUser: adminUser,
SMTPUsername: adminUser,
SMTPPassword: adminPass,
Host: mailHost,
SMTPServer: mailHost,
SMTPPort: smtpPort.Int(),
APIPort: apiPort.Int(),
TestAPIPort: apiPort.Int(),
}); err != nil {
panic(err)
}
Expand Down
19 changes: 13 additions & 6 deletions api/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package api
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"testing"

Expand Down Expand Up @@ -179,11 +181,13 @@ func TestVerifyAccountHandler(t *testing.T) {
// get the verification code from the email
mailBody, err := testMailService.FindEmail(context.Background(), testEmail)
c.Assert(err, qt.IsNil)
mailCode := strings.TrimPrefix(mailBody, VerificationCodeTextBody)
// create a regex to find the verification code in the email
mailCodeRgx := regexp.MustCompile(fmt.Sprintf(`%s(.{%d})`, VerificationCodeTextBody, VerificationCodeLength*2))
mailCode := mailCodeRgx.FindStringSubmatch(mailBody)
// verify the user
verification := mustMarshal(&UserVerification{
Email: testEmail,
Code: mailCode,
Code: mailCode[1],
})
req, err = http.NewRequest(http.MethodPost, testURL(verifyUserEndpoint), bytes.NewBuffer(verification))
c.Assert(err, qt.IsNil)
Expand Down Expand Up @@ -240,11 +244,14 @@ func TestRecoverAndResetPassword(t *testing.T) {
// get the verification code from the email
mailBody, err := testMailService.FindEmail(context.Background(), testEmail)
c.Assert(err, qt.IsNil)
verifyMailCode := strings.TrimPrefix(mailBody, VerificationCodeTextBody)
// create a regex to find the verification code in the email
mailCodeRgx := regexp.MustCompile(fmt.Sprintf(`%s(.{%d})`, VerificationCodeTextBody, VerificationCodeLength*2))
verifyMailCode := mailCodeRgx.FindStringSubmatch(mailBody)
c.Log(verifyMailCode[1])
// verify the user
verification := mustMarshal(&UserVerification{
Email: testEmail,
Code: verifyMailCode,
Code: verifyMailCode[1],
})
req, err = http.NewRequest(http.MethodPost, testURL(verifyUserEndpoint), bytes.NewBuffer(verification))
c.Assert(err, qt.IsNil)
Expand All @@ -262,12 +269,12 @@ func TestRecoverAndResetPassword(t *testing.T) {
// get the recovery code from the email
mailBody, err = testMailService.FindEmail(context.Background(), testEmail)
c.Assert(err, qt.IsNil)
passResetMailCode := strings.TrimPrefix(mailBody, VerificationCodeTextBody)
passResetMailCode := mailCodeRgx.FindStringSubmatch(mailBody)
// reset the password
newPassword := "password2"
resetPass := mustMarshal(&UserPasswordReset{
Email: testEmail,
Code: passResetMailCode,
Code: passResetMailCode[1],
NewPassword: newPassword,
})
req, err = http.NewRequest(http.MethodPost, testURL(usersResetPasswordEndpoint), bytes.NewBuffer(resetPass))
Expand Down
50 changes: 31 additions & 19 deletions cmd/service/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/vocdoni/saas-backend/account"
"github.com/vocdoni/saas-backend/api"
"github.com/vocdoni/saas-backend/db"
"github.com/vocdoni/saas-backend/notifications/sendgrid"
"github.com/vocdoni/saas-backend/notifications/smtp"
"github.com/vocdoni/saas-backend/notifications/twilio"
"go.vocdoni.io/dvote/apiclient"
"go.vocdoni.io/dvote/log"
Expand All @@ -28,12 +28,15 @@ func main() {
flag.StringP("vocdoniApi", "v", "https://api-dev.vocdoni.net/v2", "vocdoni node remote API URL")
flag.StringP("privateKey", "k", "", "private key for the Vocdoni account")
flag.BoolP("fullTransparentMode", "a", false, "allow all transactions and do not modify any of them")
flag.String("sendgridAPIKey", "", "SendGrid API key")
flag.String("sendgridFromAddress", "", "SendGrid from address")
flag.String("sendgridFromName", "Vocdoni", "SendGrid from name")
flag.String("smtpServer", "", "SMTP server")
flag.Int("smtpPort", 587, "SMTP port")
flag.String("smtpUsername", "", "SMTP username")
flag.String("smtpPassword", "", "SMTP password")
flag.String("emailFromAddress", "", "Email service from address")
flag.String("emailFromName", "Vocdoni", "Email service from name")
flag.String("twilioAccountSid", "", "Twilio account SID")
flag.String("twilioAuthToken", "", "Twilio auth token")
flag.String("twilioFromNumber", "", "Twilio from number")
flag.String("smsFromNumber", "", "SMS from number")
// parse flags
flag.Parse()
// initialize Viper
Expand All @@ -52,10 +55,13 @@ func main() {
}
mongoURL := viper.GetString("mongoURL")
mongoDB := viper.GetString("mongoDB")
// mail vars
sendgridAPIKey := viper.GetString("sendgridAPIKey")
sendgridFromAddress := viper.GetString("sendgridFromAddress")
sendgridFromName := viper.GetString("sendgridFromName")
// email vars
smtpServer := viper.GetString("smtpServer")
smtpPort := viper.GetInt("smtpPort")
smtpUsername := viper.GetString("smtpUsername")
smtpPassword := viper.GetString("smtpPassword")
emailFromAddress := viper.GetString("emailFromAddress")
emailFromName := viper.GetString("emailFromName")
// sms vars
twilioAccountSid := viper.GetString("twilioAccountSid")
twilioAuthToken := viper.GetString("twilioAuthToken")
Expand Down Expand Up @@ -93,24 +99,30 @@ func main() {
Account: acc,
FullTransparentMode: fullTransparentMode,
}
// create email notifications service if the required parameters are set and
// include it in the API configuration
if sendgridAPIKey != "" && sendgridFromAddress != "" && sendgridFromName != "" {
apiConf.MailService = new(sendgrid.SendGridEmail)
if err := apiConf.MailService.Init(&sendgrid.SendGridConfig{
FromName: sendgridFromName,
FromAddress: sendgridFromAddress,
APIKey: sendgridAPIKey,
// overwrite the email notifications service with the SMTP service if the
// required parameters are set and include it in the API configuration
if smtpServer != "" && smtpUsername != "" && smtpPassword != "" {
if emailFromAddress == "" || emailFromName == "" {
log.Fatal("emailFromAddress and emailFromName are required")
}
apiConf.MailService = new(smtp.SMTPEmail)
if err := apiConf.MailService.New(&smtp.SMTPConfig{
FromName: emailFromName,
FromAddress: emailFromAddress,
SMTPServer: smtpServer,
SMTPPort: smtpPort,
SMTPUsername: smtpUsername,
SMTPPassword: smtpPassword,
}); err != nil {
log.Fatalf("could not create the email service: %v", err)
}
log.Infow("email service created", "from", fmt.Sprintf("%s <%s>", sendgridFromName, sendgridFromAddress))
log.Infow("email service created", "from", fmt.Sprintf("%s <%s>", emailFromName, emailFromAddress))
}
// create SMS notifications service if the required parameters are set and
// include it in the API configuration
if twilioAccountSid != "" && twilioAuthToken != "" && twilioFromNumber != "" {
apiConf.SMSService = new(twilio.TwilioSMS)
if err := apiConf.SMSService.Init(&twilio.TwilioConfig{
if err := apiConf.SMSService.New(&twilio.TwilioConfig{
AccountSid: twilioAccountSid,
AuthToken: twilioAuthToken,
FromNumber: twilioFromNumber,
Expand Down
7 changes: 5 additions & 2 deletions example.env
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
VOCDONI_PORT=8080
VOCDONI_SECRET=supersecret
VOCDONI_PRIVATEKEY=vochain-private-key
VOCDONI_SENDGRIDAPIKEY=SG.1234567890
[email protected]
VOCDONI_SMTPSERVER=smpt.server.com
VOCDONI_SMTPUSERNAME=admin
VOCDONI_SMTPPASSWORD=password
[email protected]
[email protected]
2 changes: 0 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ require (
github.com/go-chi/cors v1.2.1
github.com/go-chi/jwtauth/v5 v5.3.1
github.com/lestrrat-go/jwx/v2 v2.0.20
github.com/sendgrid/sendgrid-go v3.16.0+incompatible
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.18.2
github.com/testcontainers/testcontainers-go v0.32.0
Expand Down Expand Up @@ -297,7 +296,6 @@ require (
github.com/samber/lo v1.39.0 // indirect
github.com/sasha-s/go-deadlock v0.3.1 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/sethvargo/go-retry v0.2.4 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
Expand Down
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1379,10 +1379,6 @@ github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/segmentio/kafka-go v0.1.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo=
github.com/segmentio/kafka-go v0.2.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo=
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.16.0+incompatible h1:i8eE6IMkiCy7vusSdacHHSBUpXyTcTXy/Rl9N9aZ/Qw=
github.com/sendgrid/sendgrid-go v3.16.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec=
github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw=
Expand Down
18 changes: 17 additions & 1 deletion notifications/notifications.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ package notifications

import "context"

// Notification represents a notification to be sent, it can be an email or an
// SMS. It contains the recipient's name, address, number, the subject and the
// body of the message. The recipient's name and address are used for emails,
// while the recipient's number is used for SMS.
type Notification struct {
ToName string
ToAddress string
Expand All @@ -10,7 +14,19 @@ type Notification struct {
Body string
}

// NotificationService is the interface that must be implemented by any
// notification service. It contains the methods New and SendNotification.
// Init is used to initialize the service with the configuration, and
// SendNotification is used to send a notification.
type NotificationService interface {
Init(conf any) error
// New initializes the notification service with the configuration. Each
// service implementation can have its own configuration type, which is
// passed as an argument to this method and must be casted to the correct
// type inside the method.
New(conf any) error
// SendNotification sends a notification to the recipient. The notification
// contains the recipient's name, address, number, the subject and the body
// of the message. This method cannot be blocking, so it must return an
// error if the notification could not be sent or if the context is done.
SendNotification(context.Context, *Notification) error
}
52 changes: 0 additions & 52 deletions notifications/sendgrid/email.go

This file was deleted.

Loading
Loading