From ec3677d4cefb2488665c68b45489f8514fe4a547 Mon Sep 17 00:00:00 2001 From: aradwann Date: Sun, 7 Jan 2024 20:34:26 +0200 Subject: [PATCH] impl Email Gmail Sender --- Makefile | 4 +- app.env | 5 ++- gapi/rpc_create_user.go | 2 +- go.mod | 1 + go.sum | 2 + mail/sender.go | 65 ++++++++++++++++++++++++++++++++ mail/sender_test.go | 31 +++++++++++++++ util/config.go | 3 ++ worker/task_send_verify_email.go | 9 ++--- 9 files changed, 113 insertions(+), 9 deletions(-) create mode 100644 mail/sender.go create mode 100644 mail/sender_test.go diff --git a/Makefile b/Makefile index 3d2226e..9d8168f 100644 --- a/Makefile +++ b/Makefile @@ -31,10 +31,10 @@ createmigration: migrate create -ext sql -dir $(MIGRATIONS_PATH) -seq "$(filter-out $@,$(MAKECMDGOALS))" test: - go test -v -cover ./... + go test -short -v -cover ./... testci: - go test -race -covermode atomic -coverprofile=covprofile ./... + go test -short -race -covermode atomic -coverprofile=covprofile ./... server: go run main.go diff --git a/app.env b/app.env index c11e600..8de1683 100644 --- a/app.env +++ b/app.env @@ -7,4 +7,7 @@ GRPC_SERVER_ADDRESS=0.0.0.0:9090 TOKEN_SYMMETRIC_KEY=B51FE30989F143F6F07D2CB828495D69 ACCESS_TOKEN_DURATION=15m REFRESH_TOKEN_DURATION=24h -REDIS_ADDRESS=0.0.0.0:6379 \ No newline at end of file +REDIS_ADDRESS=0.0.0.0:6379 +EMAIL_SENDER_NAME=eenergy +EMAIL_SENDER_ADDRESS=ahmedradwan9966@gmail.com +EMAIL_SENDER_PASSWORD= \ No newline at end of file diff --git a/gapi/rpc_create_user.go b/gapi/rpc_create_user.go index c93ec1b..aaa7e32 100644 --- a/gapi/rpc_create_user.go +++ b/gapi/rpc_create_user.go @@ -36,7 +36,7 @@ func (server *Server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) taskPayload := &worker.PayloadSendVerifyEmail{Username: user.Username} opts := []asynq.Option{ asynq.MaxRetry(10), - asynq.ProcessIn(10 * time.Second), + asynq.ProcessIn(10 * time.Second), // make room for the DB to commit the transaction before the task is picked up by the worker, otherwise the worker might not find the record asynq.Queue(worker.QueueCritical), } return server.taskDistributor.DistributeTaskSendVerifyEmail(ctx, taskPayload, opts...) diff --git a/go.mod b/go.mod index 770988f..060f290 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 github.com/hibiken/asynq v0.24.1 github.com/jackc/pgx/v5 v5.5.1 + github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/o1egl/paseto v1.0.0 github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.8.4 diff --git a/go.sum b/go.sum index 801f699..664cb9c 100644 --- a/go.sum +++ b/go.sum @@ -133,6 +133,8 @@ github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= diff --git a/mail/sender.go b/mail/sender.go new file mode 100644 index 0000000..7881c17 --- /dev/null +++ b/mail/sender.go @@ -0,0 +1,65 @@ +package mail + +import ( + "fmt" + "net/smtp" + + "github.com/jordan-wright/email" +) + +const ( + smtpAuthAddress = "smtp.gmail.com" + smtpServerAddress = "smtp.gmail.com:587" +) + +type EmailSender interface { + SendEmail( + subject string, + content string, + to []string, + cc []string, + bcc []string, + attachFiles []string, + ) error +} + +type GmailSender struct { + name string + fromEmailAddress string + fromEmailPassword string +} + +func NewGmailSender(name string, fromEmailAddress string, fromEmailPassword string) EmailSender { + return &GmailSender{ + name: name, + fromEmailAddress: fromEmailAddress, + fromEmailPassword: fromEmailPassword, + } +} + +func (sender *GmailSender) SendEmail( + subject string, + content string, + to []string, + cc []string, + bcc []string, + attachFiles []string, +) error { + e := email.NewEmail() + e.From = fmt.Sprintf("%s <%s>", sender.name, sender.fromEmailAddress) + e.Subject = subject + e.HTML = []byte(content) + e.To = to + e.Cc = cc + e.Bcc = bcc + + for _, f := range attachFiles { + _, err := e.AttachFile(f) + if err != nil { + return fmt.Errorf("failed to attach file %s: %w", f, err) + } + } + + smtpAuth := smtp.PlainAuth("", sender.fromEmailAddress, sender.fromEmailPassword, smtpAuthAddress) + return e.Send(smtpServerAddress, smtpAuth) +} diff --git a/mail/sender_test.go b/mail/sender_test.go new file mode 100644 index 0000000..fe4f86c --- /dev/null +++ b/mail/sender_test.go @@ -0,0 +1,31 @@ +package mail + +import ( + "testing" + + "github.com/aradwann/eenergy/util" + "github.com/stretchr/testify/require" +) + +func TestSendEmailWithGmail(t *testing.T) { + // skip when the flag is set to prevent the CI from sending emails every time it runs + if testing.Short() { + t.Skip() + } + + config, err := util.LoadConfig("..", "app") + require.NoError(t, err) + + sender := NewGmailSender(config.EmailSenderName, config.EmailSenderAddress, config.EmailSenderPassword) + + subject := "A test email" + content := ` +

Hello world

+

This is a test message from Eenergy

+ ` + to := []string{"aradwann@proton.me"} + attachFiles := []string{"../README.md"} + + err = sender.SendEmail(subject, content, to, nil, nil, attachFiles) + require.NoError(t, err) +} diff --git a/util/config.go b/util/config.go index 954792f..756d91e 100644 --- a/util/config.go +++ b/util/config.go @@ -19,6 +19,9 @@ type Config struct { TokenSymmetricKey string `mapstructure:"TOKEN_SYMMETRIC_KEY"` AccessTokenDuration time.Duration `mapstructure:"ACCESS_TOKEN_DURATION"` RefreshTokenDuration time.Duration `mapstructure:"REFRESH_TOKEN_DURATION"` + EmailSenderName string `mapstructure:"EMAIL_SENDER_NAME"` + EmailSenderAddress string `mapstructure:"EMAIL_SENDER_ADDRESS"` + EmailSenderPassword string `mapstructure:"EMAIL_SENDER_PASSWORD"` } // LoadConfig read configuration from the file or environment variables diff --git a/worker/task_send_verify_email.go b/worker/task_send_verify_email.go index 7b482b5..fefdd05 100644 --- a/worker/task_send_verify_email.go +++ b/worker/task_send_verify_email.go @@ -2,9 +2,7 @@ package worker import ( "context" - "database/sql" "encoding/json" - "errors" "fmt" "log/slog" @@ -50,10 +48,11 @@ func (processor *RedisTaskProcessor) ProcessTaskSendVerifyEmail(ctx context.Cont user, err := processor.store.GetUser(ctx, payload.Username) if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return fmt.Errorf("user doesn't exist: %w", asynq.SkipRetry) + // if the user is not found try again later, as the creation might be not commited yet + // if errors.Is(err, sql.ErrNoRows) { + // return fmt.Errorf("user doesn't exist: %w", asynq.SkipRetry) - } + // } return fmt.Errorf("failed to get user: %w", asynq.SkipRetry) } // TODO: send email to user