all repos

onasty @ d12b6ab2d5dab2f5909a2a726bad740efed0cfe6

a one-time notes service
29 files changed, 588 insertions(+), 160 deletions(-)
feat: mailer service (#55)

* chore(docker-compose): add nats container

* feat add the prototype  of the service

* feat(mailer): setup custom logger

* refactor(mailer): refactor the main

* feat(mailer): run it in the docker

* fix(mailer): apparently now the version should be embedded correctly

* refactor(mailer): send emails in background

* refactor(handlers): accept request_id

- request_id is needed for better logging, and would be helpful in case
of debug

* chore(mailer): add readme with basic doc

it's kind of ironic how the service has docs and main one dont

* feat(mailer): add metrics

* chore(prometheus): add mailer service

* chore: ignore the length of the main function

* add the comment for future me

* fix(mailer): respect finished context

* chore: update .env example file

* chore(docker-compose): use mailer's env file

* feat(core): basic integration with mailer service

* refactor(core): remove mailer package

* fixup! feat(core): basic integration with mailer service

* that remove all handling of mail in test, should be added back soon

* refactor(mailermq): use context because it can use context

* feat(events): add util func to get errs from responses

* fixup! that remove all handling of mail in test, should be added back soon

* will it fix the ci

i just dont wanna implement it all, i just want it to run

* refactor(core.mailer): add interface

* refactor(e2e): add mock for mailer, it's better to add some kind of real
service but i just dont wanna fuck around with mailgun

* refactor(mailer): remove json schemas because it's not used anywhere

* mailer: clear comments

* go mod tidy
Author: Smirnov Oleksandr ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-01-10 22:25:33 +0200
Parent: 5bfc943
M .env.example
···
        3
        3
         SERVER_PORT=8000

      
        4
        4
         PASSWORD_SALT=onasty

      
        5
        5
         NOTE_PASSWORD_SALT=secret

      
        
        6
        +

      
        
        7
        +METRICS_ENABLED=true

      
        6
        8
         METRICS_PORT=8001

      
        7
        9
         

      
        8
        10
         LOG_LEVEL=debug

      ···
        24
        26
         REDIS_ADDR="redis:6379"

      
        25
        27
         CACHE_USERS_TTL=1h

      
        26
        28
         

      
        27
        
        -MAILGUN_FROM=onasty@mail.com

      
        28
        
        -MAILGUN_DOMAI='<domain>'

      
        29
        
        -MAILGUN_API_KEY='<token>'

      
        
        29
        +NATS_URL="nats:4222"

      
        
        30
        +

      
        30
        31
         VERIFICATION_TOKEN_TTL=48h

      
        31
        32
         

      
        32
        33
         RATELIMITER_RPS=100

      
M Taskfile.yml
···
        8
        8
         

      
        9
        9
         tasks:

      
        10
        10
           run:

      
        11
        
        -    - docker compose up -d --build --remove-orphans core

      
        
        11
        +    - docker compose up -d --build --remove-orphans core mailer

      
        12
        12
         

      
        13
        13
           lint:

      
        14
        14
             - golangci-lint run

      
M cmd/server/main.go
···
        10
        10
         	"os/signal"

      
        11
        11
         

      
        12
        12
         	"github.com/gin-gonic/gin"

      
        
        13
        +	"github.com/nats-io/nats.go"

      
        13
        14
         	"github.com/olexsmir/onasty/internal/config"

      
        
        15
        +	"github.com/olexsmir/onasty/internal/events/mailermq"

      
        14
        16
         	"github.com/olexsmir/onasty/internal/hasher"

      
        15
        17
         	"github.com/olexsmir/onasty/internal/jwtutil"

      
        16
        18
         	"github.com/olexsmir/onasty/internal/logger"

      
        17
        
        -	"github.com/olexsmir/onasty/internal/mailer"

      
        18
        19
         	"github.com/olexsmir/onasty/internal/metrics"

      
        19
        20
         	"github.com/olexsmir/onasty/internal/service/notesrv"

      
        20
        21
         	"github.com/olexsmir/onasty/internal/service/usersrv"

      ···
        37
        38
         	}

      
        38
        39
         }

      
        39
        40
         

      
        40
        
        -//nolint:err113

      
        
        41
        +//nolint:err113,funlen

      
        41
        42
         func run(ctx context.Context) error {

      
        42
        43
         	ctx, cancel := context.WithCancel(ctx)

      
        43
        44
         	defer cancel()

      ···
        58
        59
         	}

      
        59
        60
         

      
        60
        61
         	// app deps

      
        
        62
        +	nc, err := nats.Connect(cfg.NatsURL)

      
        
        63
        +	if err != nil {

      
        
        64
        +		return err

      
        
        65
        +	}

      
        
        66
        +

      
        61
        67
         	psqlDB, err := psqlutil.Connect(ctx, cfg.PostgresDSN)

      
        62
        68
         	if err != nil {

      
        63
        69
         		return err

      ···
        71
        77
         	userPasswordHasher := hasher.NewSHA256Hasher(cfg.PasswordSalt)

      
        72
        78
         	notePasswordHasher := hasher.NewSHA256Hasher(cfg.NotePassowrdSalt)

      
        73
        79
         	jwtTokenizer := jwtutil.NewJWTUtil(cfg.JwtSigningKey, cfg.JwtAccessTokenTTL)

      
        74
        
        -	mailGunMailer := mailer.NewMailgun(cfg.MailgunFrom, cfg.MailgunDomain, cfg.MailgunAPIKey)

      
        
        80
        +

      
        
        81
        +	mailermq := mailermq.New(nc)

      
        75
        82
         

      
        76
        83
         	sessionrepo := sessionrepo.New(psqlDB)

      
        77
        84
         	vertokrepo := vertokrepo.New(psqlDB)

      ···
        84
        91
         		vertokrepo,

      
        85
        92
         		userPasswordHasher,

      
        86
        93
         		jwtTokenizer,

      
        87
        
        -		mailGunMailer,

      
        
        94
        +		mailermq,

      
        88
        95
         		usercache,

      
        89
        96
         		cfg.JwtRefreshTokenTTL,

      
        90
        97
         		cfg.VerificationTokenTTL,

      
M docker-compose.yml
···
        10
        10
               - 8000:8000

      
        11
        11
               - 8001:8001

      
        12
        12
         

      
        
        13
        +  mailer:

      
        
        14
        +    image: onasty:mailer

      
        
        15
        +    container_name: onasty-mailer

      
        
        16
        +    build:

      
        
        17
        +      context: .

      
        
        18
        +      dockerfile: ./mailer/Dockerfile

      
        
        19
        +    env_file: ./mailer/.env

      
        
        20
        +

      
        13
        21
           postgres:

      
        14
        22
             image: postgres:16-alpine

      
        15
        23
             container_name: onasty-postgres

      ···
        21
        29
               - .docker/postgres:/var/lib/postgresql/data

      
        22
        30
             ports:

      
        23
        31
               - 5432:5432

      
        
        32
        +

      
        
        33
        +  nats:

      
        
        34
        +    image: nats:2.10

      
        
        35
        +    ports:

      
        
        36
        +      - 4222:4222

      
        24
        37
         

      
        25
        38
           redis:

      
        26
        39
             image: redis:7.4-alpine

      
M e2e/apiv1_auth_test.go
···
        136
        136
         	)

      
        137
        137
         

      
        138
        138
         	e.Equal(http.StatusOK, httpResp.Code)

      
        139
        
        -	e.NotEmpty(e.mailer.GetLastSentEmailToEmail(email))

      
        140
        139
         }

      
        141
        140
         

      
        142
        141
         func (e *AppTestSuite) TestAuthV1_ResendVerificationEmail_wrong() {

      ···
        175
        174
         		e.Equal(httpResp.Code, t.expectedCode)

      
        176
        175
         

      
        177
        176
         		// no email should be sent

      
        178
        
        -		e.Empty(e.mailer.GetLastSentEmailToEmail(t.email))

      
        
        177
        +		// e.Empty(e.mailer.GetLastSentEmailToEmail(t.email))

      
        179
        178
         	}

      
        180
        179
         }

      
        181
        180
         

      
M e2e/e2e_test.go
···
        17
        17
         	"github.com/olexsmir/onasty/internal/hasher"

      
        18
        18
         	"github.com/olexsmir/onasty/internal/jwtutil"

      
        19
        19
         	"github.com/olexsmir/onasty/internal/logger"

      
        20
        
        -	"github.com/olexsmir/onasty/internal/mailer"

      
        21
        20
         	"github.com/olexsmir/onasty/internal/service/notesrv"

      
        22
        21
         	"github.com/olexsmir/onasty/internal/service/usersrv"

      
        23
        22
         	"github.com/olexsmir/onasty/internal/store/psql/noterepo"

      ···
        57
        56
         		router       http.Handler

      
        58
        57
         		hasher       hasher.Hasher

      
        59
        58
         		jwtTokenizer jwtutil.JWTTokenizer

      
        60
        
        -		mailer       *mailer.TestMailer

      
        61
        59
         	}

      
        62
        60
         	errorResponse struct {

      
        63
        61
         		Message string `json:"message"`

      ···
        102
        100
         

      
        103
        101
         	e.hasher = hasher.NewSHA256Hasher(cfg.PasswordSalt)

      
        104
        102
         	e.jwtTokenizer = jwtutil.NewJWTUtil(cfg.JwtSigningKey, time.Hour)

      
        105
        
        -	e.mailer = mailer.NewTestMailer()

      
        106
        103
         

      
        107
        104
         	sessionrepo := sessionrepo.New(e.postgresDB)

      
        108
        105
         	vertokrepo := vertokrepo.New(e.postgresDB)

      ···
        115
        112
         		vertokrepo,

      
        116
        113
         		e.hasher,

      
        117
        114
         		e.jwtTokenizer,

      
        118
        
        -		e.mailer,

      
        
        115
        +		newMailerMockService(),

      
        119
        116
         		usercache,

      
        120
        117
         		cfg.JwtRefreshTokenTTL,

      
        121
        118
         		cfg.VerificationTokenTTL,

      
A e2e/mailer_test.go
···
        
        1
        +package e2e_test

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +

      
        
        6
        +	"github.com/olexsmir/onasty/internal/events/mailermq"

      
        
        7
        +)

      
        
        8
        +

      
        
        9
        +var _ mailermq.Mailer = (*mailerMockService)(nil)

      
        
        10
        +

      
        
        11
        +type mailerMockService struct{}

      
        
        12
        +

      
        
        13
        +func newMailerMockService() *mailerMockService {

      
        
        14
        +	return &mailerMockService{}

      
        
        15
        +}

      
        
        16
        +

      
        
        17
        +func (m mailerMockService) SendVerificationEmail(

      
        
        18
        +	_ context.Context,

      
        
        19
        +	_ mailermq.SendVerificationEmailRequest,

      
        
        20
        +) error {

      
        
        21
        +	return nil

      
        
        22
        +}

      
M go.mod
···
        11
        11
         	github.com/jackc/pgx-gofrs-uuid v0.0.0-20230224015001-1d428863c2e2

      
        12
        12
         	github.com/jackc/pgx/v5 v5.7.2

      
        13
        13
         	github.com/mailgun/mailgun-go/v4 v4.21.0

      
        
        14
        +	github.com/nats-io/nats.go v1.38.0

      
        14
        15
         	github.com/prometheus/client_golang v1.20.5

      
        15
        16
         	github.com/redis/go-redis/v9 v9.7.0

      
        16
        17
         	github.com/stretchr/testify v1.10.0

      ···
        85
        86
         	github.com/modern-go/reflect2 v1.0.2 // indirect

      
        86
        87
         	github.com/morikuni/aec v1.0.0 // indirect

      
        87
        88
         	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect

      
        
        89
        +	github.com/nats-io/nkeys v0.4.9 // indirect

      
        
        90
        +	github.com/nats-io/nuid v1.0.1 // indirect

      
        88
        91
         	github.com/opencontainers/go-digest v1.0.0 // indirect

      
        89
        92
         	github.com/opencontainers/image-spec v1.1.0 // indirect

      
        90
        93
         	github.com/pelletier/go-toml/v2 v2.2.2 // indirect

      
M go.sum
···
        235
        235
         github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=

      
        236
        236
         github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=

      
        237
        237
         github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=

      
        
        238
        +github.com/nats-io/nats.go v1.38.0 h1:A7P+g7Wjp4/NWqDOOP/K6hfhr54DvdDQUznt5JFg9XA=

      
        
        239
        +github.com/nats-io/nats.go v1.38.0/go.mod h1:IGUM++TwokGnXPs82/wCuiHS02/aKrdYUQkU8If6yjw=

      
        
        240
        +github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0=

      
        
        241
        +github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE=

      
        
        242
        +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=

      
        
        243
        +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=

      
        238
        244
         github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=

      
        239
        245
         github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=

      
        240
        246
         github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=

      
M infra/prometheus/prometheus.yml
···
        10
        10
             follow_redirects: true

      
        11
        11
             honor_timestamps: true

      
        12
        12
             static_configs:

      
        13
        
        -    - targets: [core:8001]

      
        
        13
        +      - targets: [core:8001]

      
        
        14
        +        labels:

      
        
        15
        +          service: "core"

      
        
        16
        +

      
        
        17
        +  - job_name: mailer

      
        
        18
        +    metrics_path: /metrics

      
        
        19
        +    scheme: http

      
        
        20
        +    follow_redirects: true

      
        
        21
        +    honor_labels: true

      
        
        22
        +    static_configs:

      
        
        23
        +      - targets: [mailer:8001]

      
        
        24
        +        labels:

      
        
        25
        +          service: "mailer"

      
M internal/config/config.go
···
        11
        11
         	AppEnv     string

      
        12
        12
         	AppURL     string

      
        13
        13
         	ServerPort string

      
        
        14
        +	NatsURL    string

      
        14
        15
         

      
        15
        16
         	PostgresDSN      string

      
        16
        17
         	PasswordSalt     string

      ···
        48
        49
         		AppEnv:     getenvOrDefault("APP_ENV", "debug"),

      
        49
        50
         		AppURL:     getenvOrDefault("APP_URL", ""),

      
        50
        51
         		ServerPort: getenvOrDefault("SERVER_PORT", "3000"),

      
        
        52
        +		NatsURL:    getenvOrDefault("NATS_URL", ""),

      
        51
        53
         

      
        52
        54
         		PostgresDSN:      getenvOrDefault("POSTGRESQL_DSN", ""),

      
        53
        55
         		PasswordSalt:     getenvOrDefault("PASSWORD_SALT", ""),

      
A internal/events/events.go
···
        
        1
        +package events

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"fmt"

      
        
        5
        +

      
        
        6
        +	"github.com/nats-io/nats.go"

      
        
        7
        +)

      
        
        8
        +

      
        
        9
        +const (

      
        
        10
        +	natsHeaderErrorCode = "Nats-Service-Error-Code"

      
        
        11
        +	natsHeaderErrorMsg  = "Nats-Service-Error"

      
        
        12
        +)

      
        
        13
        +

      
        
        14
        +var _ error = (*Error)(nil)

      
        
        15
        +

      
        
        16
        +type Error struct {

      
        
        17
        +	Code    string

      
        
        18
        +	Message string

      
        
        19
        +}

      
        
        20
        +

      
        
        21
        +func (e Error) Error() string {

      
        
        22
        +	return fmt.Sprintf("code: %s; msg: %s", e.Code, e.Message)

      
        
        23
        +}

      
        
        24
        +

      
        
        25
        +func CheckRespForError(resp *nats.Msg) error {

      
        
        26
        +	code := resp.Header.Get(natsHeaderErrorCode)

      
        
        27
        +	msg := resp.Header.Get(natsHeaderErrorMsg)

      
        
        28
        +	if code == "" && msg == "" {

      
        
        29
        +		return nil

      
        
        30
        +	}

      
        
        31
        +

      
        
        32
        +	return &Error{

      
        
        33
        +		Code:    code,

      
        
        34
        +		Message: msg,

      
        
        35
        +	}

      
        
        36
        +}

      
A internal/events/mailermq/mailermq.go
···
        
        1
        +package mailermq

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"encoding/json"

      
        
        6
        +

      
        
        7
        +	"github.com/nats-io/nats.go"

      
        
        8
        +	"github.com/olexsmir/onasty/internal/events"

      
        
        9
        +	"github.com/olexsmir/onasty/internal/transport/http/reqid"

      
        
        10
        +)

      
        
        11
        +

      
        
        12
        +type Mailer interface {

      
        
        13
        +	SendVerificationEmail(ctx context.Context, input SendVerificationEmailRequest) error

      
        
        14
        +}

      
        
        15
        +

      
        
        16
        +type MailerMQ struct {

      
        
        17
        +	nc *nats.Conn

      
        
        18
        +}

      
        
        19
        +

      
        
        20
        +const sendMailSubject = "mailer.send"

      
        
        21
        +

      
        
        22
        +func New(nc *nats.Conn) *MailerMQ {

      
        
        23
        +	return &MailerMQ{

      
        
        24
        +		nc: nc,

      
        
        25
        +	}

      
        
        26
        +}

      
        
        27
        +

      
        
        28
        +type sendRequest struct {

      
        
        29
        +	RequestID    string            `json:"request_id"`

      
        
        30
        +	Receiver     string            `json:"receiver"`

      
        
        31
        +	TemplateName string            `json:"template_name"`

      
        
        32
        +	Options      map[string]string `json:"options"`

      
        
        33
        +}

      
        
        34
        +

      
        
        35
        +type SendVerificationEmailRequest struct {

      
        
        36
        +	Receiver string

      
        
        37
        +	Token    string

      
        
        38
        +}

      
        
        39
        +

      
        
        40
        +func (m MailerMQ) SendVerificationEmail(

      
        
        41
        +	ctx context.Context,

      
        
        42
        +	inp SendVerificationEmailRequest,

      
        
        43
        +) error {

      
        
        44
        +	req, err := json.Marshal(sendRequest{

      
        
        45
        +		RequestID:    reqid.GetContext(ctx),

      
        
        46
        +		Receiver:     inp.Receiver,

      
        
        47
        +		TemplateName: "email_verification",

      
        
        48
        +		Options: map[string]string{

      
        
        49
        +			"token": inp.Token,

      
        
        50
        +		},

      
        
        51
        +	})

      
        
        52
        +	if err != nil {

      
        
        53
        +		return err

      
        
        54
        +	}

      
        
        55
        +

      
        
        56
        +	resp, err := m.nc.RequestWithContext(ctx, sendMailSubject, req)

      
        
        57
        +	if err != nil {

      
        
        58
        +		return err

      
        
        59
        +	}

      
        
        60
        +

      
        
        61
        +	return events.CheckRespForError(resp)

      
        
        62
        +}

      
D internal/mailer/mailer.go
···
        1
        
        -package mailer

      
        2
        
        -

      
        3
        
        -import "context"

      
        4
        
        -

      
        5
        
        -type Mailer interface {

      
        6
        
        -	Send(ctx context.Context, to, subject, content string) error

      
        7
        
        -}

      
M internal/mailer/mailgun.gomailer/mailgun.go
···
        1
        
        -package mailer

      
        
        1
        +package main

      
        2
        2
         

      
        3
        3
         import (

      
        4
        4
         	"context"

      
        5
        5
         	"log/slog"

      
        6
        6
         

      
        7
        7
         	"github.com/mailgun/mailgun-go/v4"

      
        8
        
        -	"github.com/olexsmir/onasty/internal/metrics"

      
        9
        8
         	"github.com/olexsmir/onasty/internal/transport/http/reqid"

      
        10
        9
         )

      
        
        10
        +

      
        
        11
        +type Mailer interface {

      
        
        12
        +	Send(ctx context.Context, to, subject, content string) error

      
        
        13
        +}

      
        11
        14
         

      
        12
        15
         var _ Mailer = (*Mailgun)(nil)

      
        13
        16
         

      ···
        33
        36
         

      
        34
        37
         	_, _, err := m.mg.Send(ctx, msg)

      
        35
        38
         	if err != nil {

      
        36
        
        -		metrics.RecordEmailFailed(reqid.GetContext(ctx))

      
        
        39
        +		RecordEmailFailed(reqid.GetContext(ctx))

      
        37
        40
         		return err

      
        38
        41
         	}

      
        39
        42
         

      
        40
        43
         	slog.DebugContext(ctx, "email sent", "subject", subject, "content", content, "err", err)

      
        41
        44
         	slog.InfoContext(ctx, "email sent", "to", to)

      
        42
        
        -	metrics.RecordEmailSent()

      
        
        45
        +

      
        
        46
        +	RecordEmailSent()

      
        43
        47
         

      
        44
        48
         	return nil

      
        45
        49
         }

      
D internal/mailer/testing_mailer.go
···
        1
        
        -package mailer

      
        2
        
        -

      
        3
        
        -import (

      
        4
        
        -	"context"

      
        5
        
        -	"sync"

      
        6
        
        -)

      
        7
        
        -

      
        8
        
        -var _ Mailer = (*TestMailer)(nil)

      
        9
        
        -

      
        10
        
        -type TestMailer struct {

      
        11
        
        -	mu sync.Mutex

      
        12
        
        -

      
        13
        
        -	emails map[string]string

      
        14
        
        -}

      
        15
        
        -

      
        16
        
        -// NewTestMailer create a mailer for tests

      
        17
        
        -// that implementation of Mailer stores all sent email in memory

      
        18
        
        -// to get the last email sent to a specific email use GetLastSentEmailToEmail

      
        19
        
        -func NewTestMailer() *TestMailer {

      
        20
        
        -	return &TestMailer{ //nolint:exhaustruct

      
        21
        
        -		emails: make(map[string]string),

      
        22
        
        -	}

      
        23
        
        -}

      
        24
        
        -

      
        25
        
        -func (t *TestMailer) Send(_ context.Context, to, _, content string) error {

      
        26
        
        -	t.mu.Lock()

      
        27
        
        -	defer t.mu.Unlock()

      
        28
        
        -

      
        29
        
        -	t.emails[to] = content

      
        30
        
        -

      
        31
        
        -	return nil

      
        32
        
        -}

      
        33
        
        -

      
        34
        
        -// GetLastSentEmailToEmail returns the last email sent to a specific email

      
        35
        
        -func (t *TestMailer) GetLastSentEmailToEmail(email string) string {

      
        36
        
        -	t.mu.Lock()

      
        37
        
        -	defer t.mu.Unlock()

      
        38
        
        -

      
        39
        
        -	e := t.emails[email]

      
        40
        
        -

      
        41
        
        -	return e

      
        42
        
        -}

      
D internal/mailer/testing_mailer_test.go
···
        1
        
        -package mailer

      
        2
        
        -

      
        3
        
        -import (

      
        4
        
        -	"context"

      
        5
        
        -	"testing"

      
        6
        
        -

      
        7
        
        -	"github.com/stretchr/testify/assert"

      
        8
        
        -	"github.com/stretchr/testify/require"

      
        9
        
        -)

      
        10
        
        -

      
        11
        
        -func TestMailer_Send(t *testing.T) {

      
        12
        
        -	m := NewTestMailer()

      
        13
        
        -	assert.Empty(t, m.emails)

      
        14
        
        -

      
        15
        
        -	email := "test@mail.com"

      
        16
        
        -	err := m.Send(context.TODO(), email, "", "content")

      
        17
        
        -	require.NoError(t, err)

      
        18
        
        -

      
        19
        
        -	assert.Equal(t, "content", m.emails[email])

      
        20
        
        -}

      
        21
        
        -

      
        22
        
        -func TestMailer_GetLastSentEmailToEmail(t *testing.T) {

      
        23
        
        -	email := "test@mail.com"

      
        24
        
        -	content := "content"

      
        25
        
        -

      
        26
        
        -	m := NewTestMailer()

      
        27
        
        -	assert.Empty(t, m.emails)

      
        28
        
        -

      
        29
        
        -	m.emails[email] = content

      
        30
        
        -

      
        31
        
        -	c := m.GetLastSentEmailToEmail(email)

      
        32
        
        -	assert.Equal(t, content, c)

      
        33
        
        -}

      
D internal/service/usersrv/email.go
···
        1
        
        -package usersrv

      
        2
        
        -

      
        3
        
        -import (

      
        4
        
        -	"context"

      
        5
        
        -	"errors"

      
        6
        
        -	"fmt"

      
        7
        
        -	"log/slog"

      
        8
        
        -)

      
        9
        
        -

      
        10
        
        -var ErrFailedToSendVerifcationEmail = errors.New("failed to send verification email")

      
        11
        
        -

      
        12
        
        -const (

      
        13
        
        -	verificationEmailSubject = "Onasty: verify your email"

      
        14
        
        -	verificationEmailBody    = `To verify your email, please follow this link:

      
        15
        
        -<a href="%[1]s/api/v1/auth/verify/%[2]s">%[1]s/api/v1/auth/verify/%[2]s</a>

      
        16
        
        -<br />

      
        17
        
        -<br />

      
        18
        
        -This link will expire after 24 hours.`

      
        19
        
        -)

      
        20
        
        -

      
        21
        
        -func (u *UserSrv) sendVerificationEmail(

      
        22
        
        -	ctx context.Context,

      
        23
        
        -	cancel context.CancelFunc,

      
        24
        
        -	userEmail string,

      
        25
        
        -	token string,

      
        26
        
        -	url string,

      
        27
        
        -) {

      
        28
        
        -	select {

      
        29
        
        -	case <-ctx.Done():

      
        30
        
        -		slog.ErrorContext(ctx, "failed to send verification email", "err", ctx.Err())

      
        31
        
        -	default:

      
        32
        
        -		if err := u.mailer.Send(

      
        33
        
        -			ctx,

      
        34
        
        -			userEmail,

      
        35
        
        -			verificationEmailSubject,

      
        36
        
        -			fmt.Sprintf(verificationEmailBody, url, token),

      
        37
        
        -		); err != nil {

      
        38
        
        -			slog.ErrorContext(ctx, "failed to send verification email", "err", err)

      
        39
        
        -		}

      
        40
        
        -		cancel()

      
        41
        
        -	}

      
        42
        
        -}

      
M internal/service/usersrv/usersrv.go
···
        8
        8
         

      
        9
        9
         	"github.com/gofrs/uuid/v5"

      
        10
        10
         	"github.com/olexsmir/onasty/internal/dtos"

      
        
        11
        +	"github.com/olexsmir/onasty/internal/events/mailermq"

      
        11
        12
         	"github.com/olexsmir/onasty/internal/hasher"

      
        12
        13
         	"github.com/olexsmir/onasty/internal/jwtutil"

      
        13
        
        -	"github.com/olexsmir/onasty/internal/mailer"

      
        14
        14
         	"github.com/olexsmir/onasty/internal/models"

      
        15
        15
         	"github.com/olexsmir/onasty/internal/store/psql/sessionrepo"

      
        16
        16
         	"github.com/olexsmir/onasty/internal/store/psql/userepo"

      
        17
        17
         	"github.com/olexsmir/onasty/internal/store/psql/vertokrepo"

      
        18
        18
         	"github.com/olexsmir/onasty/internal/store/rdb/usercache"

      
        19
        
        -	"github.com/olexsmir/onasty/internal/transport/http/reqid"

      
        20
        19
         )

      
        21
        20
         

      
        22
        21
         type UserServicer interface {

      ···
        44
        43
         	vertokrepo   vertokrepo.VerificationTokenStorer

      
        45
        44
         	hasher       hasher.Hasher

      
        46
        45
         	jwtTokenizer jwtutil.JWTTokenizer

      
        47
        
        -	mailer       mailer.Mailer

      
        
        46
        +	mailermq     mailermq.Mailer

      
        48
        47
         	cache        usercache.UserCacheer

      
        49
        48
         

      
        50
        49
         	refreshTokenTTL      time.Duration

      ···
        58
        57
         	vertokrepo vertokrepo.VerificationTokenStorer,

      
        59
        58
         	hasher hasher.Hasher,

      
        60
        59
         	jwtTokenizer jwtutil.JWTTokenizer,

      
        61
        
        -	mailer mailer.Mailer,

      
        
        60
        +	mailermq mailermq.Mailer,

      
        62
        61
         	cache usercache.UserCacheer,

      
        63
        62
         	refreshTokenTTL, verificationTokenTTL time.Duration,

      
        64
        63
         	appURL string,

      ···
        69
        68
         		vertokrepo:           vertokrepo,

      
        70
        69
         		hasher:               hasher,

      
        71
        70
         		jwtTokenizer:         jwtTokenizer,

      
        72
        
        -		mailer:               mailer,

      
        
        71
        +		mailermq:             mailermq,

      
        73
        72
         		cache:                cache,

      
        74
        73
         		refreshTokenTTL:      refreshTokenTTL,

      
        75
        74
         		verificationTokenTTL: verificationTokenTTL,

      ···
        99
        98
         		return uuid.Nil, err

      
        100
        99
         	}

      
        101
        100
         

      
        102
        
        -	sendingCtx, cancel := getContextForEmailSending(ctx)

      
        103
        
        -	go u.sendVerificationEmail(sendingCtx, cancel, inp.Email, vtok, u.appURL)

      
        
        101
        +	if err := u.mailermq.SendVerificationEmail(ctx, mailermq.SendVerificationEmailRequest{

      
        
        102
        +		Receiver: inp.Email,

      
        
        103
        +		Token:    vtok,

      
        
        104
        +	}); err != nil {

      
        
        105
        +		return uuid.Nil, err

      
        
        106
        +	}

      
        104
        107
         

      
        105
        108
         	return uid, nil

      
        106
        109
         }

      ···
        221
        224
         		return err

      
        222
        225
         	}

      
        223
        226
         

      
        224
        
        -	sendingCtx, cancel := getContextForEmailSending(ctx)

      
        225
        
        -	go u.sendVerificationEmail(sendingCtx, cancel, inp.Email, token, u.appURL)

      
        
        227
        +	if err := u.mailermq.SendVerificationEmail(ctx, mailermq.SendVerificationEmailRequest{

      
        
        228
        +		Receiver: inp.Email,

      
        
        229
        +		Token:    token,

      
        
        230
        +	}); err != nil {

      
        
        231
        +		return err

      
        
        232
        +	}

      
        226
        233
         

      
        227
        234
         	return nil

      
        228
        235
         }

      ···
        285
        292
         		Refresh: refreshToken,

      
        286
        293
         	}, err

      
        287
        294
         }

      
        288
        
        -

      
        289
        
        -func getContextForEmailSending(ctx context.Context) (context.Context, context.CancelFunc) {

      
        290
        
        -	rid := reqid.GetContext(ctx)

      
        291
        
        -	resCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)

      
        292
        
        -	resCtx = reqid.SetContext(resCtx, rid)

      
        293
        
        -

      
        294
        
        -	return resCtx, cancel

      
        295
        
        -}

      
A mailer/.env.example
···
        
        1
        +APP_URL=http://localhost:8000

      
        
        2
        +NATS_URL="nats:4222"

      
        
        3
        +METRICS_ENABLED=true

      
        
        4
        +METRICS_PORT=8001

      
        
        5
        +LOG_LEVEL=debug

      
        
        6
        +LOG_FORMAT=text

      
        
        7
        +LOG_SHOW_LINE=true

      
        
        8
        +MAILGUN_FROM=onasty@mail.com

      
        
        9
        +MAILGUN_DOMAI='<domain>'

      
        
        10
        +MAILGUN_API_KEY='<token>'

      
A mailer/Dockerfile
···
        
        1
        +FROM golang:1.23.3-alpine AS builder

      
        
        2
        +

      
        
        3
        +WORKDIR /app

      
        
        4
        +

      
        
        5
        +COPY go.mod go.sum ./

      
        
        6
        +RUN go mod download

      
        
        7
        +

      
        
        8
        +COPY internal internal

      
        
        9
        +COPY mailer mailer

      
        
        10
        +

      
        
        11
        +ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64

      
        
        12
        +RUN go build -trimpath -ldflags='-w -s' -o /mailer ./mailer

      
        
        13
        +

      
        
        14
        +

      
        
        15
        +FROM alpine:3.20

      
        
        16
        +COPY --from=builder /mailer /mailer

      
        
        17
        +RUN apk --no-cache add ca-certificates

      
        
        18
        +ENTRYPOINT ["/mailer"]

      
A mailer/README.md
···
        
        1
        +# mailer service

      
        
        2
        +

      
        
        3
        +All templates could be found *[here](./template.go)*

      
        
        4
        +

      
        
        5
        +## endpoints

      
        
        6
        +### `mailer.ping`

      
        
        7
        +This endpoint always returns pong message

      
        
        8
        +

      
        
        9
        +Response:

      
        
        10
        +```json

      
        
        11
        +{

      
        
        12
        +  "message": "pong"

      
        
        13
        +}

      
        
        14
        +```

      
        
        15
        +

      
        
        16
        +### `mailer.send`

      
        
        17
        +

      
        
        18
        +Input

      
        
        19
        +- `request_id` : *string* - (optional) the request id, needed to keep consistency across services

      
        
        20
        +- `receiver` : *string* - the email receiver

      
        
        21
        +- `template_name` : *string* - the template that's going to be used

      
        
        22
        +- `options` : *Map<string, string>* - template specific options

      
        
        23
        +

      
        
        24
        +

      
        
        25
        +Example input

      
        
        26
        +```json

      
        
        27
        +{

      
        
        28
        +  "request_id": "hello_world",

      
        
        29
        +  "receiver": "onasty@example.com",

      
        
        30
        +  "template_name": "email_verification",

      
        
        31
        +  "options": {

      
        
        32
        +    "token": "the_verification_token"

      
        
        33
        +  }

      
        
        34
        +}

      
        
        35
        +```

      
        
        36
        +

      
        
        37
        +#### Template specific options

      
        
        38
        +- `email_verification`

      
        
        39
        +  - `token` the token that is used in verification link

      
        
        40
        +

      
        
        41
        +

      
A mailer/config.go
···
        
        1
        +package main

      
        
        2
        +

      
        
        3
        +import "os"

      
        
        4
        +

      
        
        5
        +type Config struct {

      
        
        6
        +	AppURL        string

      
        
        7
        +	NatsURL       string

      
        
        8
        +	MailgunFrom   string

      
        
        9
        +	MailgunDomain string

      
        
        10
        +	MailgunAPIKey string

      
        
        11
        +

      
        
        12
        +	LogLevel    string

      
        
        13
        +	LogFormat   string

      
        
        14
        +	LogShowLine bool

      
        
        15
        +

      
        
        16
        +	MetricsEnabled bool

      
        
        17
        +	MetricsPort    string

      
        
        18
        +}

      
        
        19
        +

      
        
        20
        +func NewConfig() *Config {

      
        
        21
        +	return &Config{

      
        
        22
        +		AppURL:         getenvOrDefault("APP_URL", ""),

      
        
        23
        +		NatsURL:        getenvOrDefault("NATS_URL", ""),

      
        
        24
        +		MailgunFrom:    getenvOrDefault("MAILGUN_FROM", ""),

      
        
        25
        +		MailgunDomain:  getenvOrDefault("MAILGUN_DOMAIN", ""),

      
        
        26
        +		MailgunAPIKey:  getenvOrDefault("MAILGUN_API_KEY", ""),

      
        
        27
        +		LogLevel:       getenvOrDefault("LOG_LEVEL", "debug"),

      
        
        28
        +		LogFormat:      getenvOrDefault("LOG_FORMAT", "json"),

      
        
        29
        +		LogShowLine:    getenvOrDefault("LOG_SHOW_LINE", "true") == "true",

      
        
        30
        +		MetricsPort:    getenvOrDefault("METRICS_PORT", ""),

      
        
        31
        +		MetricsEnabled: getenvOrDefault("METRICS_ENABLED", "true") == "true",

      
        
        32
        +	}

      
        
        33
        +}

      
        
        34
        +

      
        
        35
        +func getenvOrDefault(key, def string) string {

      
        
        36
        +	if v, ok := os.LookupEnv(key); ok {

      
        
        37
        +		return v

      
        
        38
        +	}

      
        
        39
        +	return def

      
        
        40
        +}

      
A mailer/handlers.go
···
        
        1
        +package main

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"encoding/json"

      
        
        6
        +	"log/slog"

      
        
        7
        +	"time"

      
        
        8
        +

      
        
        9
        +	"github.com/nats-io/nats.go/micro"

      
        
        10
        +	"github.com/olexsmir/onasty/internal/transport/http/reqid"

      
        
        11
        +)

      
        
        12
        +

      
        
        13
        +type Handlers struct {

      
        
        14
        +	service *Service

      
        
        15
        +}

      
        
        16
        +

      
        
        17
        +func NewHandlers(service *Service) *Handlers {

      
        
        18
        +	return &Handlers{

      
        
        19
        +		service: service,

      
        
        20
        +	}

      
        
        21
        +}

      
        
        22
        +

      
        
        23
        +func (h Handlers) RegisterAll(svc micro.Service) error {

      
        
        24
        +	m := svc.AddGroup("mailer")

      
        
        25
        +	if err := m.AddEndpoint("ping", micro.HandlerFunc(h.pingHandler)); err != nil {

      
        
        26
        +		return err

      
        
        27
        +	}

      
        
        28
        +

      
        
        29
        +	if err := m.AddEndpoint("send", micro.HandlerFunc(h.sendHandler)); err != nil {

      
        
        30
        +		return err

      
        
        31
        +	}

      
        
        32
        +

      
        
        33
        +	return nil

      
        
        34
        +}

      
        
        35
        +

      
        
        36
        +type pingResponse struct {

      
        
        37
        +	Message string `json:"message"`

      
        
        38
        +}

      
        
        39
        +

      
        
        40
        +func (h Handlers) pingHandler(req micro.Request) {

      
        
        41
        +	_ = req.RespondJSON(pingResponse{

      
        
        42
        +		Message: "pong",

      
        
        43
        +	})

      
        
        44
        +}

      
        
        45
        +

      
        
        46
        +type sendRequest struct {

      
        
        47
        +	RequestID string `json:"request_id"`

      
        
        48
        +

      
        
        49
        +	Receiver     string            `json:"receiver"`

      
        
        50
        +	TemplateName string            `json:"template_name"`

      
        
        51
        +	Options      map[string]string `json:"options"`

      
        
        52
        +}

      
        
        53
        +

      
        
        54
        +func (h Handlers) sendHandler(req micro.Request) {

      
        
        55
        +	var inp sendRequest

      
        
        56
        +	if err := json.Unmarshal(req.Data(), &inp); err != nil {

      
        
        57
        +		slog.Error("failed to unmarshal input data", "err", err)

      
        
        58
        +		return

      
        
        59
        +	}

      
        
        60
        +

      
        
        61
        +	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)

      
        
        62
        +	ctx = reqid.SetContext(ctx, inp.RequestID)

      
        
        63
        +

      
        
        64
        +	if err := h.service.Send(ctx, cancel, inp.Receiver, inp.TemplateName, inp.Options); err != nil {

      
        
        65
        +		_ = req.Error("500", err.Error(), nil)

      
        
        66
        +	}

      
        
        67
        +

      
        
        68
        +	_ = req.Respond(nil)

      
        
        69
        +}

      
A mailer/main.go
···
        
        1
        +package main

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"errors"

      
        
        5
        +	"fmt"

      
        
        6
        +	"log/slog"

      
        
        7
        +	"net/http"

      
        
        8
        +	"os"

      
        
        9
        +	"os/signal"

      
        
        10
        +	"strings"

      
        
        11
        +	"syscall"

      
        
        12
        +

      
        
        13
        +	"github.com/nats-io/nats.go"

      
        
        14
        +	"github.com/nats-io/nats.go/micro"

      
        
        15
        +	"github.com/olexsmir/onasty/internal/logger"

      
        
        16
        +	"github.com/olexsmir/onasty/internal/transport/http/httpserver"

      
        
        17
        +

      
        
        18
        +	_ "embed"

      
        
        19
        +)

      
        
        20
        +

      
        
        21
        +//go:embed version

      
        
        22
        +var _version string

      
        
        23
        +

      
        
        24
        +var version = strings.Trim(_version, "\n")

      
        
        25
        +

      
        
        26
        +func main() {

      
        
        27
        +	if err := run(); err != nil {

      
        
        28
        +		fmt.Fprintf(os.Stderr, "error: %v\n", err)

      
        
        29
        +		os.Exit(1)

      
        
        30
        +	}

      
        
        31
        +}

      
        
        32
        +

      
        
        33
        +func run() error {

      
        
        34
        +	cfg := NewConfig()

      
        
        35
        +	nc, err := nats.Connect(cfg.NatsURL)

      
        
        36
        +	if err != nil {

      
        
        37
        +		return err

      
        
        38
        +	}

      
        
        39
        +

      
        
        40
        +	logger, err := logger.NewCustomLogger(cfg.LogLevel, cfg.LogFormat, cfg.LogShowLine)

      
        
        41
        +	if err != nil {

      
        
        42
        +		return err

      
        
        43
        +	}

      
        
        44
        +

      
        
        45
        +	slog.SetDefault(logger)

      
        
        46
        +

      
        
        47
        +	//nolint:exhaustruct

      
        
        48
        +	svc, err := micro.AddService(nc, micro.Config{

      
        
        49
        +		Name:    "mailer",

      
        
        50
        +		Version: version,

      
        
        51
        +	})

      
        
        52
        +	if err != nil {

      
        
        53
        +		return err

      
        
        54
        +	}

      
        
        55
        +

      
        
        56
        +	mg := NewMailgun(cfg.MailgunFrom, cfg.MailgunDomain, cfg.MailgunAPIKey)

      
        
        57
        +	service := NewService(cfg.AppURL, mg)

      
        
        58
        +	handlers := NewHandlers(service)

      
        
        59
        +

      
        
        60
        +	if err := handlers.RegisterAll(svc); err != nil {

      
        
        61
        +		return err

      
        
        62
        +	}

      
        
        63
        +

      
        
        64
        +	if cfg.MetricsEnabled {

      
        
        65
        +		srv := httpserver.NewServer(cfg.MetricsPort, MetricsHandler())

      
        
        66
        +		go func() {

      
        
        67
        +			slog.Info("starting metrics server", "port", cfg.MetricsPort)

      
        
        68
        +			if err := srv.Start(); !errors.Is(err, http.ErrServerClosed) {

      
        
        69
        +				slog.Error("failed to start metrics server", "error", err)

      
        
        70
        +			}

      
        
        71
        +		}()

      
        
        72
        +	}

      
        
        73
        +

      
        
        74
        +	slog.Info("the service is listening")

      
        
        75
        +

      
        
        76
        +	// graceful shutdown

      
        
        77
        +	quitCh := make(chan os.Signal, 1)

      
        
        78
        +	signal.Notify(quitCh, syscall.SIGINT, syscall.SIGTERM)

      
        
        79
        +	<-quitCh

      
        
        80
        +

      
        
        81
        +	slog.Info("stopping the service")

      
        
        82
        +

      
        
        83
        +	if err := svc.Stop(); err != nil {

      
        
        84
        +		return err

      
        
        85
        +	}

      
        
        86
        +

      
        
        87
        +	if err := nc.Drain(); err != nil {

      
        
        88
        +		return err

      
        
        89
        +	}

      
        
        90
        +

      
        
        91
        +	return nil

      
        
        92
        +}

      
A mailer/metrics.go
···
        
        1
        +package main

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"net/http"

      
        
        5
        +

      
        
        6
        +	"github.com/prometheus/client_golang/prometheus"

      
        
        7
        +	"github.com/prometheus/client_golang/prometheus/promauto"

      
        
        8
        +	"github.com/prometheus/client_golang/prometheus/promhttp"

      
        
        9
        +)

      
        
        10
        +

      
        
        11
        +var (

      
        
        12
        +	emailSentSuccessfully = promauto.NewCounter(prometheus.CounterOpts{ //nolint:exhaustruct

      
        
        13
        +		Name: "mail_sent_total",

      
        
        14
        +		Help: "the total number of successfully sent email",

      
        
        15
        +	})

      
        
        16
        +

      
        
        17
        +	emailFailedToSend = promauto.NewCounterVec(prometheus.CounterOpts{ //nolint:exhaustruct

      
        
        18
        +		Name: "mail_failed_total",

      
        
        19
        +		Help: "the total number of email that failed to send",

      
        
        20
        +	}, []string{"request_id"})

      
        
        21
        +)

      
        
        22
        +

      
        
        23
        +func MetricsHandler() http.Handler {

      
        
        24
        +	mux := http.NewServeMux()

      
        
        25
        +	mux.Handle("GET /metrics", promhttp.Handler())

      
        
        26
        +	return mux

      
        
        27
        +}

      
        
        28
        +

      
        
        29
        +func RecordEmailSent() {

      
        
        30
        +	go emailSentSuccessfully.Inc()

      
        
        31
        +}

      
        
        32
        +

      
        
        33
        +func RecordEmailFailed(reqid string) {

      
        
        34
        +	go emailFailedToSend.With(prometheus.Labels{

      
        
        35
        +		"request_id": reqid,

      
        
        36
        +	}).Inc()

      
        
        37
        +}

      
A mailer/service.go
···
        
        1
        +package main

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"log/slog"

      
        
        6
        +)

      
        
        7
        +

      
        
        8
        +type Service struct {

      
        
        9
        +	appURL string

      
        
        10
        +	mg     *Mailgun

      
        
        11
        +}

      
        
        12
        +

      
        
        13
        +func NewService(appURL string, mg *Mailgun) *Service {

      
        
        14
        +	return &Service{

      
        
        15
        +		appURL: appURL,

      
        
        16
        +		mg:     mg,

      
        
        17
        +	}

      
        
        18
        +}

      
        
        19
        +

      
        
        20
        +func (s Service) Send(

      
        
        21
        +	ctx context.Context,

      
        
        22
        +	cancel context.CancelFunc,

      
        
        23
        +	receiver, templateName string,

      
        
        24
        +	templateOpts map[string]string,

      
        
        25
        +) error {

      
        
        26
        +	tmpl, err := getTemplate(s.appURL, templateName)

      
        
        27
        +	if err != nil {

      
        
        28
        +		return err

      
        
        29
        +	}

      
        
        30
        +

      
        
        31
        +	t := tmpl(templateOpts)

      
        
        32
        +

      
        
        33
        +	go func() {

      
        
        34
        +		select {

      
        
        35
        +		case <-ctx.Done():

      
        
        36
        +			slog.ErrorContext(ctx, "failed to send verification email", "err", ctx.Err())

      
        
        37
        +			return

      
        
        38
        +		default:

      
        
        39
        +			if err := s.mg.Send(ctx, receiver, t.Subject, t.Body); err != nil {

      
        
        40
        +				slog.ErrorContext(ctx, "failed to send verification email", "err", err)

      
        
        41
        +			}

      
        
        42
        +			cancel()

      
        
        43
        +		}

      
        
        44
        +	}()

      
        
        45
        +

      
        
        46
        +	return nil

      
        
        47
        +}

      
A mailer/template.go
···
        
        1
        +package main

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"errors"

      
        
        5
        +	"fmt"

      
        
        6
        +)

      
        
        7
        +

      
        
        8
        +type Template struct {

      
        
        9
        +	Subject string

      
        
        10
        +	Body    string

      
        
        11
        +}

      
        
        12
        +

      
        
        13
        +type TemplateFunc func(args map[string]string) Template

      
        
        14
        +

      
        
        15
        +func getTemplate(appURL string, templateName string) (TemplateFunc, error) {

      
        
        16
        +	if templateName == "email_verification" {

      
        
        17
        +		return emailVerificationTemplate(appURL), nil

      
        
        18
        +	}

      
        
        19
        +

      
        
        20
        +	return nil, errors.New("failed to get template") //nolint:err113

      
        
        21
        +}

      
        
        22
        +

      
        
        23
        +func emailVerificationTemplate(appURL string) TemplateFunc {

      
        
        24
        +	return func(opts map[string]string) Template {

      
        
        25
        +		return Template{

      
        
        26
        +			Subject: "Onasty: verify your email",

      
        
        27
        +			Body: fmt.Sprintf(`To verify your email, please follow this link:

      
        
        28
        +<a href="%[1]s/api/v1/auth/verify/%[2]s">%[1]s/api/v1/auth/verify/%[2]s</a>

      
        
        29
        +<br />

      
        
        30
        +<br />

      
        
        31
        +This link will expire after 24 hours.`, appURL, opts["token"]),

      
        
        32
        +		}

      
        
        33
        +	}

      
        
        34
        +}

      
A mailer/version
···
        
        1
        +0.1.0