diff --git a/compose.yaml b/compose.yaml index 65873be60..3d3162ce4 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,4 +1,3 @@ - services: postgres: image: "postgres:16" @@ -75,6 +74,14 @@ services: volumes: - "./compose/tempo/tempo.yaml:/etc/tempo.yaml:ro" - "tempo-data:/var/tempo:rw" + mailhog: + image: "mailhog/mailhog:latest" + platform: linux/amd64 + ports: + - "1025:1025" # SMTP server + - "8025:8025" # Web UI + environment: + - MH_STORAGE=memory volumes: postgres-data: @@ -82,4 +89,3 @@ volumes: grafana-data: prometheus-data: tempo-data: - diff --git a/go.mod b/go.mod index 6b43e0931..e3b9ccd7d 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/go-chi/chi/v5 v5.2.1 github.com/go-chi/cors v1.2.1 github.com/jackc/pgx/v5 v5.7.2 + github.com/jhillyerd/enmime v1.3.0 github.com/prometheus/client_golang v1.21.1 github.com/vektah/gqlparser/v2 v2.5.23 go.gearno.de/crypto/uuid v0.1.0 @@ -33,11 +34,13 @@ require ( github.com/aws/smithy-go v1.22.3 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect @@ -45,13 +48,19 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect github.com/klauspost/compress v1.18.0 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/rivo/uniseg v0.4.4 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sosodev/duration v1.3.1 // indirect + github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/urfave/cli/v2 v2.27.5 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.gearno.de/x/panicf v0.1.1 // indirect diff --git a/go.sum b/go.sum index 84b3aaa6e..25507801c 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= +github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= @@ -60,8 +62,12 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= +github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs= +github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -83,6 +89,10 @@ github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA= +github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= +github.com/jhillyerd/enmime v1.3.0 h1:LV5kzfLidiOr8qRGIpYYmUZCnhrPbcFAnAFUnWn99rw= +github.com/jhillyerd/enmime v1.3.0/go.mod h1:6c6jg5HdRRV2FtvVL69LjiX1M8oE0xDX9VEhV3oy4gs= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -91,8 +101,15 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= @@ -103,6 +120,9 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -111,6 +131,8 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= diff --git a/pkg/coredata/email.go b/pkg/coredata/email.go new file mode 100644 index 000000000..bbfb6b426 --- /dev/null +++ b/pkg/coredata/email.go @@ -0,0 +1,133 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/getprobo/probo/pkg/gid" + "github.com/jackc/pgx/v5" + "go.gearno.de/kit/pg" +) + +type ( + Email struct { + ID gid.GID `db:"id"` + RecipientEmail string `db:"recipient_email"` + RecipientName string `db:"recipient_name"` + Subject string `db:"subject"` + TextBody string `db:"text_body"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + SentAt *time.Time `db:"sent_at"` + } +) + +var ( + ErrNoUnsentEmail = errors.New("no unsent email found") +) + +func NewEmail( + recipientName string, + recipientEmail string, + subject string, + body string, +) *Email { + return &Email{ + ID: gid.New(gid.NilTenant, EmailEntityType), + RecipientName: recipientName, + RecipientEmail: recipientEmail, + Subject: subject, + TextBody: body, + } +} + +func (e *Email) Insert( + ctx context.Context, + conn pg.Conn, +) error { + q := ` +INSERT INTO emails (id, recipient_email, recipient_name, subject, text_body, created_at, updated_at) +VALUES (@id, @recipient_email, @recipient_name, @subject, @text_body, @created_at, @updated_at) + ` + + args := pgx.StrictNamedArgs{ + "id": e.ID, + "recipient_email": e.RecipientEmail, + "recipient_name": e.RecipientName, + "subject": e.Subject, + "text_body": e.TextBody, + "created_at": e.CreatedAt, + "updated_at": e.UpdatedAt, + } + + _, err := conn.Exec(ctx, q, args) + return err +} + +func (e *Email) LoadNextUnsentForUpdate( + ctx context.Context, + conn pg.Conn, +) error { + q := ` +SELECT id, recipient_email, recipient_name, subject, text_body, created_at, updated_at, sent_at +FROM emails +WHERE sent_at IS NULL +ORDER BY created_at ASC +LIMIT 1 +FOR UPDATE + ` + + rows, err := conn.Query(ctx, q) + if err != nil { + return err + } + + email, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Email]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrNoUnsentEmail + } + + return fmt.Errorf("cannot collect email: %w", err) + } + + *e = email + + return nil +} + +func (e *Email) Update( + ctx context.Context, + conn pg.Conn, +) error { + q := ` +UPDATE emails +SET sent_at = @sent_at, updated_at = @updated_at +WHERE id = @id + ` + + args := pgx.StrictNamedArgs{ + "id": e.ID, + "sent_at": e.SentAt, + "updated_at": e.UpdatedAt, + } + + _, err := conn.Exec(ctx, q, args) + return err +} diff --git a/pkg/coredata/entity_type_reg.go b/pkg/coredata/entity_type_reg.go index 27b55f19e..b6e1b76a0 100644 --- a/pkg/coredata/entity_type_reg.go +++ b/pkg/coredata/entity_type_reg.go @@ -28,4 +28,5 @@ const ( PolicyEntityType UserEntityType SessionEntityType + EmailEntityType ) diff --git a/pkg/coredata/migrations/20250311T183100Z.sql b/pkg/coredata/migrations/20250311T183100Z.sql new file mode 100644 index 000000000..1526eff4c --- /dev/null +++ b/pkg/coredata/migrations/20250311T183100Z.sql @@ -0,0 +1,11 @@ +CREATE TABLE emails ( + id TEXT PRIMARY KEY, + email_address_to TEXT NOT NULL, + subject TEXT NOT NULL, + text_body TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + sent_at TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX ON emails (sent_at) WHERE sent_at IS NULL; diff --git a/pkg/coredata/migrations/20250312T084900Z.sql b/pkg/coredata/migrations/20250312T084900Z.sql new file mode 100644 index 000000000..6393f4cd2 --- /dev/null +++ b/pkg/coredata/migrations/20250312T084900Z.sql @@ -0,0 +1,8 @@ +ALTER TABLE emails ADD COLUMN recipient_name TEXT NOT NULL; +ALTER TABLE emails RENAME COLUMN email_address_to TO recipient_email; + +ALTER TABLE emails ALTER COLUMN created_at DROP DEFAULT; +ALTER TABLE emails ALTER COLUMN updated_at DROP DEFAULT; + +ALTER TABLE emails ALTER COLUMN created_at SET NOT NULL; +ALTER TABLE emails ALTER COLUMN updated_at SET NOT NULL; diff --git a/pkg/mailer/mailer.go b/pkg/mailer/mailer.go new file mode 100644 index 000000000..610227150 --- /dev/null +++ b/pkg/mailer/mailer.go @@ -0,0 +1,115 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package mailer + +import ( + "bytes" + "context" + "errors" + "fmt" + "net/smtp" + "time" + + "github.com/getprobo/probo/pkg/coredata" + "github.com/jhillyerd/enmime" + "go.gearno.de/kit/log" + "go.gearno.de/kit/pg" +) + +type ( + Mailer struct { + pg *pg.Client + l *log.Logger + cfg Config + } + + Config struct { + SenderName string + SenderEmail string + Addr string + } +) + +func NewMailer(pg *pg.Client, l *log.Logger, cfg Config) *Mailer { + return &Mailer{pg: pg, l: l, cfg: cfg} +} + +func (m *Mailer) Run(ctx context.Context) error { +LOOP: + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(1 * time.Second): + ctx := context.Background() + if err := m.batchSendEmails(ctx); err != nil { + m.l.ErrorCtx(ctx, "cannot send email", log.Error(err)) + } + + goto LOOP + } +} + +func (m *Mailer) batchSendEmails(ctx context.Context) error { + for { + err := m.pg.WithTx( + ctx, + func(tx pg.Conn) error { + email := &coredata.Email{} + err := email.LoadNextUnsentForUpdate(ctx, tx) + if err != nil { + return err + } + + mail := enmime.Builder(). + Subject(email.Subject). + From(m.cfg.SenderName, m.cfg.SenderEmail). + To(email.RecipientName, email.RecipientEmail). + Text([]byte(email.TextBody)) + + envelope, err := mail.Build() + if err != nil { + return fmt.Errorf("cannot build email: %w", err) + } + + var buf bytes.Buffer + if err := envelope.Encode(&buf); err != nil { + return fmt.Errorf("cannot encode email: %w", err) + } + + if err := smtp.SendMail(m.cfg.Addr, nil, m.cfg.SenderEmail, []string{email.RecipientEmail}, buf.Bytes()); err != nil { + return fmt.Errorf("cannot send email: %w", err) + } + + now := time.Now() + email.SentAt = &now + email.UpdatedAt = now + + if err := email.Update(ctx, tx); err != nil { + return fmt.Errorf("cannot update email: %w", err) + } + + return nil + }, + ) + + if errors.Is(err, coredata.ErrNoUnsentEmail) { + return nil + } + + if err != nil { + return err + } + } +} diff --git a/pkg/probod/mailer_config.go b/pkg/probod/mailer_config.go new file mode 100644 index 000000000..20903b1b7 --- /dev/null +++ b/pkg/probod/mailer_config.go @@ -0,0 +1,29 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package probod + +type ( + mailerConfig struct { + SenderName string `json:"sender-name"` + SenderEmail string `json:"sender-email"` + SMTP smtpConfig `json:"smtp"` + } + + smtpConfig struct { + Addr string `json:"addr"` + User string `json:"user"` + Password string `json:"password"` + } +) diff --git a/pkg/probod/probod.go b/pkg/probod/probod.go index fe11f2c2f..130825e44 100644 --- a/pkg/probod/probod.go +++ b/pkg/probod/probod.go @@ -27,6 +27,7 @@ import ( "github.com/getprobo/probo/pkg/awsconfig" "github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/crypto/passwdhash" + "github.com/getprobo/probo/pkg/mailer" "github.com/getprobo/probo/pkg/probo" "github.com/getprobo/probo/pkg/server" console_v1 "github.com/getprobo/probo/pkg/server/api/console/v1" @@ -47,10 +48,11 @@ type ( } config struct { - Pg pgConfig `json:"pg"` - Api apiConfig `json:"api"` - Auth authConfig `json:"auth"` - AWS awsConfig `json:"aws"` + Pg pgConfig `json:"pg"` + Api apiConfig `json:"api"` + Auth authConfig `json:"auth"` + AWS awsConfig `json:"aws"` + Mailer mailerConfig `json:"mailer"` } ) @@ -94,6 +96,13 @@ func New() *Implm { SecretAccessKey: "thisisnotasecret", Endpoint: "http://127.0.0.1:9000", }, + Mailer: mailerConfig{ + SenderEmail: "no-reply@notification.getprobo.com", + SenderName: "Probo", + SMTP: smtpConfig{ + Addr: "localhost:1025", + }, + }, }, } } @@ -198,8 +207,23 @@ func (impl *Implm) Run( } }() + mailerCtx, stopMailer := context.WithCancel(context.Background()) + mailer := mailer.NewMailer(pgClient, l, mailer.Config{ + SenderEmail: impl.cfg.Mailer.SenderEmail, + SenderName: impl.cfg.Mailer.SenderName, + Addr: impl.cfg.Mailer.SMTP.Addr, + }) + wg.Add(1) + go func() { + defer wg.Done() + if err := mailer.Run(mailerCtx); err != nil { + cancel(fmt.Errorf("mailer crashed: %w", err)) + } + }() + <-ctx.Done() + stopMailer() stopApiServer() wg.Wait()