diff --git a/.depot.cache.json b/.depot.cache.json index 90ac562..f45a181 100644 --- a/.depot.cache.json +++ b/.depot.cache.json @@ -1 +1 @@ -[{"t":"go","n":"github.com/beorn7/perks","v":"v1.0.1","l":["MIT"]},{"t":"go","n":"github.com/caarlos0/env/v6","v":"v6.10.1","l":["MIT"]},{"t":"go","n":"github.com/cespare/xxhash/v2","v":"v2.1.2","l":["MIT"]},{"t":"go","n":"github.com/cespare/xxhash/v2","v":"v2.3.0","l":["MIT"]},{"t":"go","n":"github.com/davecgh/go-spew","v":"v1.1.1","l":["ISC"]},{"t":"go","n":"github.com/golang-jwt/jwt","v":"v3.2.2+incompatible","l":["MIT"]},{"t":"go","n":"github.com/golang/protobuf","v":"v1.5.2","l":["BSD-3-Clause"]},{"t":"go","n":"github.com/google/uuid","v":"v1.6.0","l":["BSD-3-Clause"]},{"t":"go","n":"github.com/grafana/regexp","v":"v0.0.0-20240518133315-a468a5bfb3bc","l":["BSD-3-Clause"]},{"t":"go","n":"github.com/json-iterator/go","v":"v1.1.12","l":["MIT"]},{"t":"go","n":"github.com/keighl/mandrill","v":"v0.0.0-20170605120353-1775dd4b3b41","l":["~unknown"]},{"t":"go","n":"github.com/labstack/echo-contrib","v":"v0.13.0","l":["MIT"]},{"t":"go","n":"github.com/labstack/echo-contrib","v":"v0.17.4","l":["MIT"]},{"t":"go","n":"github.com/labstack/echo/v4","v":"v4.13.4","l":["MIT"]},{"t":"go","n":"github.com/labstack/echo/v4","v":"v4.9.1","l":["MIT"]},{"t":"go","n":"github.com/labstack/gommon","v":"v0.4.0","l":["MIT"]},{"t":"go","n":"github.com/labstack/gommon","v":"v0.4.2","l":["MIT"]},{"t":"go","n":"github.com/mailjet/mailjet-apiv3-go/v3","v":"v3.2.0","l":["MIT"]},{"t":"go","n":"github.com/mattn/go-colorable","v":"v0.1.12","l":["MIT"]},{"t":"go","n":"github.com/mattn/go-colorable","v":"v0.1.14","l":["MIT"]},{"t":"go","n":"github.com/mattn/go-isatty","v":"v0.0.14","l":["MIT"]},{"t":"go","n":"github.com/mattn/go-isatty","v":"v0.0.20","l":["MIT"]},{"t":"go","n":"github.com/matttproud/golang_protobuf_extensions","v":"v1.0.1","l":["Apache-2.0"]},{"t":"go","n":"github.com/modern-go/concurrent","v":"v0.0.0-20180306012644-bacd9c7ef1dd","l":["Apache-2.0"]},{"t":"go","n":"github.com/modern-go/reflect2","v":"v1.0.2","l":["Apache-2.0"]},{"t":"go","n":"github.com/modfin/brev","v":"v0.0.0-20250417042907-0cc746d8fd74","l":["~unknown"]},{"t":"go","n":"github.com/modfin/henry","v":"v1.0.1","l":["MIT"]},{"t":"go","n":"github.com/munnerz/goautoneg","v":"v0.0.0-20191010083416-a7dc8b61c822","l":["BSD-3-Clause"]},{"t":"go","n":"github.com/opentracing/opentracing-go","v":"v1.2.0","l":["Apache-2.0"]},{"t":"go","n":"github.com/pkg/errors","v":"v0.9.1","l":["BSD-2-Clause"]},{"t":"go","n":"github.com/pmezard/go-difflib","v":"v1.0.0","l":["BSD-3-Clause"]},{"t":"go","n":"github.com/prometheus/client_golang","v":"v1.13.0","l":["Apache-2.0"]},{"t":"go","n":"github.com/prometheus/client_golang","v":"v1.23.0","l":["Apache-2.0","BSD-3-Clause"]},{"t":"go","n":"github.com/prometheus/client_model","v":"v0.2.0","l":["Apache-2.0"]},{"t":"go","n":"github.com/prometheus/client_model","v":"v0.6.2","l":["Apache-2.0"]},{"t":"go","n":"github.com/prometheus/common","v":"v0.37.0","l":["Apache-2.0"]},{"t":"go","n":"github.com/prometheus/common","v":"v0.66.0","l":["Apache-2.0"]},{"t":"go","n":"github.com/prometheus/procfs","v":"v0.17.0","l":["Apache-2.0"]},{"t":"go","n":"github.com/prometheus/procfs","v":"v0.8.0","l":["Apache-2.0"]},{"t":"go","n":"github.com/sendgrid/rest","v":"v2.6.5+incompatible","l":["MIT"]},{"t":"go","n":"github.com/sendgrid/rest","v":"v2.6.9+incompatible","l":["MIT"]},{"t":"go","n":"github.com/sendgrid/sendgrid-go","v":"v3.12.0+incompatible","l":["MIT"]},{"t":"go","n":"github.com/sendgrid/sendgrid-go","v":"v3.16.1+incompatible","l":["MIT"]},{"t":"go","n":"github.com/stretchr/objx","v":"v0.5.2","l":["MIT"]},{"t":"go","n":"github.com/stretchr/testify","v":"v1.11.1","l":["MIT"]},{"t":"go","n":"github.com/uber/jaeger-client-go","v":"v2.30.0+incompatible","l":["Apache-2.0"]},{"t":"go","n":"github.com/uber/jaeger-lib","v":"v2.4.1+incompatible","l":["Apache-2.0"]},{"t":"go","n":"github.com/valyala/bytebufferpool","v":"v1.0.0","l":["MIT"]},{"t":"go","n":"github.com/valyala/fasttemplate","v":"v1.2.1","l":["MIT"]},{"t":"go","n":"github.com/valyala/fasttemplate","v":"v1.2.2","l":["MIT"]},{"t":"go","n":"go.uber.org/atomic","v":"v1.11.0","l":["MIT"]},{"t":"go","n":"go.uber.org/atomic","v":"v1.9.0","l":["MIT"]},{"t":"go","n":"golang.org/x/crypto","v":"v0.0.0-20220722155217-630584e8d5aa","l":["BSD-3-Clause"]},{"t":"go","n":"golang.org/x/crypto","v":"v0.41.0","l":["BSD-3-Clause"]},{"t":"go","n":"golang.org/x/net","v":"v0.0.0-20220728030405-41545e8bf201","l":["BSD-3-Clause"]},{"t":"go","n":"golang.org/x/net","v":"v0.43.0","l":["BSD-3-Clause"]},{"t":"go","n":"golang.org/x/sys","v":"v0.0.0-20220728004956-3c1f35247d10","l":["BSD-3-Clause"]},{"t":"go","n":"golang.org/x/sys","v":"v0.35.0","l":["BSD-3-Clause"]},{"t":"go","n":"golang.org/x/text","v":"v0.28.0","l":["BSD-3-Clause"]},{"t":"go","n":"golang.org/x/text","v":"v0.3.8","l":["BSD-3-Clause"]},{"t":"go","n":"golang.org/x/time","v":"v0.0.0-20220722155302-e5dcc9cfc0b9","l":["BSD-3-Clause"]},{"t":"go","n":"golang.org/x/time","v":"v0.12.0","l":["BSD-3-Clause"]},{"t":"go","n":"google.golang.org/protobuf","v":"v1.28.1","l":["BSD-3-Clause"]},{"t":"go","n":"google.golang.org/protobuf","v":"v1.36.8","l":["BSD-3-Clause"]},{"t":"go","n":"gopkg.in/yaml.v2","v":"v2.4.0","l":["Apache-2.0"]},{"t":"go","n":"gopkg.in/yaml.v3","v":"v3.0.1","l":["MIT","Apache-2.0"]}] \ No newline at end of file +[{"t":"go","n":"github.com/beorn7/perks","v":"v1.0.1","l":["MIT"]},{"t":"go","n":"github.com/caarlos0/env/v6","v":"v6.10.1","l":["MIT"]},{"t":"go","n":"github.com/cespare/xxhash/v2","v":"v2.1.2","l":["MIT"]},{"t":"go","n":"github.com/cespare/xxhash/v2","v":"v2.3.0","l":["MIT"]},{"t":"go","n":"github.com/davecgh/go-spew","v":"v1.1.1","l":["ISC"]},{"t":"go","n":"github.com/gobuffalo/envy","v":"v1.10.2","l":["MIT"]},{"t":"go","n":"github.com/golang-jwt/jwt","v":"v3.2.2+incompatible","l":["MIT"]},{"t":"go","n":"github.com/golang/protobuf","v":"v1.5.2","l":["BSD-3-Clause"]},{"t":"go","n":"github.com/google/uuid","v":"v1.6.0","l":["BSD-3-Clause"]},{"t":"go","n":"github.com/grafana/regexp","v":"v0.0.0-20240518133315-a468a5bfb3bc","l":["BSD-3-Clause"]},{"t":"go","n":"github.com/joho/godotenv","v":"v1.4.0","l":["MIT"]},{"t":"go","n":"github.com/json-iterator/go","v":"v1.1.12","l":["MIT"]},{"t":"go","n":"github.com/keighl/mandrill","v":"v0.0.0-20170605120353-1775dd4b3b41","l":["~unknown"]},{"t":"go","n":"github.com/labstack/echo-contrib","v":"v0.13.0","l":["MIT"]},{"t":"go","n":"github.com/labstack/echo-contrib","v":"v0.17.4","l":["MIT"]},{"t":"go","n":"github.com/labstack/echo/v4","v":"v4.13.4","l":["MIT"]},{"t":"go","n":"github.com/labstack/echo/v4","v":"v4.9.1","l":["MIT"]},{"t":"go","n":"github.com/labstack/gommon","v":"v0.4.0","l":["MIT"]},{"t":"go","n":"github.com/labstack/gommon","v":"v0.4.2","l":["MIT"]},{"t":"go","n":"github.com/mailgun/mailgun-go","v":"v2.0.0+incompatible","l":["BSD-3-Clause"]},{"t":"go","n":"github.com/mailjet/mailjet-apiv3-go/v3","v":"v3.2.0","l":["MIT"]},{"t":"go","n":"github.com/mattn/go-colorable","v":"v0.1.12","l":["MIT"]},{"t":"go","n":"github.com/mattn/go-colorable","v":"v0.1.14","l":["MIT"]},{"t":"go","n":"github.com/mattn/go-isatty","v":"v0.0.14","l":["MIT"]},{"t":"go","n":"github.com/mattn/go-isatty","v":"v0.0.20","l":["MIT"]},{"t":"go","n":"github.com/matttproud/golang_protobuf_extensions","v":"v1.0.1","l":["Apache-2.0"]},{"t":"go","n":"github.com/modern-go/concurrent","v":"v0.0.0-20180306012644-bacd9c7ef1dd","l":["Apache-2.0"]},{"t":"go","n":"github.com/modern-go/reflect2","v":"v1.0.2","l":["Apache-2.0"]},{"t":"go","n":"github.com/modfin/brev","v":"v0.0.0-20250417042907-0cc746d8fd74","l":["~unknown"]},{"t":"go","n":"github.com/modfin/henry","v":"v1.0.1","l":["MIT"]},{"t":"go","n":"github.com/munnerz/goautoneg","v":"v0.0.0-20191010083416-a7dc8b61c822","l":["BSD-3-Clause"]},{"t":"go","n":"github.com/opentracing/opentracing-go","v":"v1.2.0","l":["Apache-2.0"]},{"t":"go","n":"github.com/pkg/errors","v":"v0.9.1","l":["BSD-2-Clause"]},{"t":"go","n":"github.com/pmezard/go-difflib","v":"v1.0.0","l":["BSD-3-Clause"]},{"t":"go","n":"github.com/prometheus/client_golang","v":"v1.13.0","l":["Apache-2.0"]},{"t":"go","n":"github.com/prometheus/client_golang","v":"v1.23.0","l":["Apache-2.0","BSD-3-Clause"]},{"t":"go","n":"github.com/prometheus/client_model","v":"v0.2.0","l":["Apache-2.0"]},{"t":"go","n":"github.com/prometheus/client_model","v":"v0.6.2","l":["Apache-2.0"]},{"t":"go","n":"github.com/prometheus/common","v":"v0.37.0","l":["Apache-2.0"]},{"t":"go","n":"github.com/prometheus/common","v":"v0.66.0","l":["Apache-2.0"]},{"t":"go","n":"github.com/prometheus/procfs","v":"v0.17.0","l":["Apache-2.0"]},{"t":"go","n":"github.com/prometheus/procfs","v":"v0.8.0","l":["Apache-2.0"]},{"t":"go","n":"github.com/rogpeppe/go-internal","v":"v1.10.0","l":["BSD-3-Clause"]},{"t":"go","n":"github.com/sendgrid/rest","v":"v2.6.5+incompatible","l":["MIT"]},{"t":"go","n":"github.com/sendgrid/rest","v":"v2.6.9+incompatible","l":["MIT"]},{"t":"go","n":"github.com/sendgrid/sendgrid-go","v":"v3.12.0+incompatible","l":["MIT"]},{"t":"go","n":"github.com/sendgrid/sendgrid-go","v":"v3.16.1+incompatible","l":["MIT"]},{"t":"go","n":"github.com/stretchr/objx","v":"v0.5.2","l":["MIT"]},{"t":"go","n":"github.com/stretchr/testify","v":"v1.11.1","l":["MIT"]},{"t":"go","n":"github.com/uber/jaeger-client-go","v":"v2.30.0+incompatible","l":["Apache-2.0"]},{"t":"go","n":"github.com/uber/jaeger-lib","v":"v2.4.1+incompatible","l":["Apache-2.0"]},{"t":"go","n":"github.com/valyala/bytebufferpool","v":"v1.0.0","l":["MIT"]},{"t":"go","n":"github.com/valyala/fasttemplate","v":"v1.2.1","l":["MIT"]},{"t":"go","n":"github.com/valyala/fasttemplate","v":"v1.2.2","l":["MIT"]},{"t":"go","n":"go.uber.org/atomic","v":"v1.11.0","l":["MIT"]},{"t":"go","n":"go.uber.org/atomic","v":"v1.9.0","l":["MIT"]},{"t":"go","n":"golang.org/x/crypto","v":"v0.0.0-20220722155217-630584e8d5aa","l":["BSD-3-Clause"]},{"t":"go","n":"golang.org/x/crypto","v":"v0.41.0","l":["BSD-3-Clause"]},{"t":"go","n":"golang.org/x/net","v":"v0.0.0-20220728030405-41545e8bf201","l":["BSD-3-Clause"]},{"t":"go","n":"golang.org/x/net","v":"v0.43.0","l":["BSD-3-Clause"]},{"t":"go","n":"golang.org/x/sys","v":"v0.0.0-20220728004956-3c1f35247d10","l":["BSD-3-Clause"]},{"t":"go","n":"golang.org/x/sys","v":"v0.35.0","l":["BSD-3-Clause"]},{"t":"go","n":"golang.org/x/text","v":"v0.28.0","l":["BSD-3-Clause"]},{"t":"go","n":"golang.org/x/text","v":"v0.3.8","l":["BSD-3-Clause"]},{"t":"go","n":"golang.org/x/time","v":"v0.0.0-20220722155302-e5dcc9cfc0b9","l":["BSD-3-Clause"]},{"t":"go","n":"golang.org/x/time","v":"v0.12.0","l":["BSD-3-Clause"]},{"t":"go","n":"google.golang.org/protobuf","v":"v1.28.1","l":["BSD-3-Clause"]},{"t":"go","n":"google.golang.org/protobuf","v":"v1.36.8","l":["BSD-3-Clause"]},{"t":"go","n":"gopkg.in/yaml.v2","v":"v2.4.0","l":["Apache-2.0"]},{"t":"go","n":"gopkg.in/yaml.v3","v":"v3.0.1","l":["MIT","Apache-2.0"]}] \ No newline at end of file diff --git a/LICENSES_DEP b/LICENSES_DEP index c1e0544..3f1d0a3 100644 --- a/LICENSES_DEP +++ b/LICENSES_DEP @@ -1,8 +1,8 @@ --- Apache-2.0: 9 -BSD-3-Clause: 12 +BSD-3-Clause: 14 ISC: 1 -MIT: 18 +MIT: 20 --- ======================================================================== Apache-2.0 @@ -30,8 +30,10 @@ BSD-3-Clause github.com/keighl/mandrill v0.0.0-20170605120353-1775dd4b3b41 github.com/prometheus/client_golang v1.23.0 github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc //indirect + github.com/mailgun/mailgun-go v2.0.0+incompatible //indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 //indirect github.com/pmezard/go-difflib v1.0.0 //indirect + github.com/rogpeppe/go-internal v1.10.0 //indirect golang.org/x/crypto v0.41.0 //indirect golang.org/x/net v0.43.0 //indirect golang.org/x/sys v0.35.0 //indirect @@ -63,6 +65,8 @@ MIT github.com/stretchr/testify v1.11.1 github.com/beorn7/perks v1.0.1 //indirect github.com/cespare/xxhash/v2 v2.3.0 //indirect + github.com/gobuffalo/envy v1.10.2 //indirect + github.com/joho/godotenv v1.4.0 //indirect github.com/labstack/gommon v0.4.2 //indirect github.com/mattn/go-colorable v0.1.14 //indirect github.com/mattn/go-isatty v0.0.20 //indirect diff --git a/cmd/mmailerd/mmailerd.go b/cmd/mmailerd/mmailerd.go index 1bb92a1..3e05dff 100644 --- a/cmd/mmailerd/mmailerd.go +++ b/cmd/mmailerd/mmailerd.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/subtle" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -29,6 +30,7 @@ import ( "github.com/modfin/mmailer/internal/svc" "github.com/modfin/mmailer/services/brev" "github.com/modfin/mmailer/services/generic" + "github.com/modfin/mmailer/services/mailgun" "github.com/modfin/mmailer/services/mailjet" "github.com/modfin/mmailer/services/mandrill" "github.com/modfin/mmailer/services/sendgrid" @@ -89,11 +91,11 @@ func main() { parts[1] = strings.TrimSpace(config.Get().FromDomainOverride) mail.From.Email = strings.Join(parts, "@") } - preferredService := c.QueryParam("X-Service") + preferredService := c.Request().Header.Get("X-Service") if len(preferredService) > 0 { - ctx = logger.AddToLogContext(ctx, "preferredService", preferredService) + ctx = logger.AddToLogContext(ctx, "preferred_service", preferredService) } - res, err := facade.Send(ctx, mail, c.Request().Header.Get("X-Service")) + res, err := facade.Send(ctx, mail, preferredService) if err != nil { logger.ErrorCtx(ctx, err, "could not send email") return c.String(http.StatusInternalServerError, "could not send email") @@ -294,6 +296,33 @@ func loadServices() { } logger.Info(fmt.Sprintf(" - Mandrill: add the following posthook url %s", posthookUrl)) services = append(services, decorate(mandrill.New(parts[1]))) + case "mailgun": + if len(parts) != 2 { + logger.Warn("mailgun api string is not valid,", s) + continue + } + apiKeys := slicez.Map(domainApiKeys[service], func(k mmailer.ServiceApiKey) mmailer.ApiKey { + return k.ApiKey + }) + for _, k := range domainApiKeys[service] { + logger.Info(fmt.Sprintf(" - Mailgun: key enabled: %s", k.Domain)) + for k, v := range k.Props { + logger.Info(fmt.Sprintf(" - Mailgun: property: %s=%s", k, v)) + } + } + if len(apiKeys) == 0 { + logger.Warn(" - Mailgun: disabled, no api keys provided") + continue + } + webhookSigningKey := parts[1] + _, err := hex.DecodeString(webhookSigningKey) + if err != nil { + logger.Warn(" - Mailgun: disabled, bad webhook signing key") + continue + } + + logger.Info(fmt.Sprintf(" - Mailgun: add the following posthook url %s", posthookUrl)) + services = append(services, decorate(mailgun.New(apiKeys, webhookSigningKey))) case "sendgrid": if len(parts) < 1 || len(parts) > 2 { logger.Warn("sendgrid api string is not valid,", s) diff --git a/go.mod b/go.mod index 780d278..9653dfd 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/keighl/mandrill v0.0.0-20170605120353-1775dd4b3b41 github.com/labstack/echo-contrib v0.17.4 github.com/labstack/echo/v4 v4.13.4 + github.com/mailgun/mailgun-go/v5 v5.8.0 github.com/mailjet/mailjet-apiv3-go/v3 v3.2.0 github.com/modfin/brev v0.0.0-20250417042907-0cc746d8fd74 github.com/modfin/henry v1.0.1 @@ -24,11 +25,13 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/labstack/gommon v0.4.2 // indirect + github.com/mailgun/errors v0.4.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oapi-codegen/runtime v1.1.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.0 // indirect diff --git a/go.sum b/go.sum index 9c9dd3c..60516c6 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -32,6 +34,10 @@ github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcX github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/mailgun/errors v0.4.0 h1:6LFBvod6VIW83CMIOT9sYNp28TCX0NejFPP4dSX++i8= +github.com/mailgun/errors v0.4.0/go.mod h1:xGBaaKdEdQT0/FhwvoXv4oBaqqmVZz9P1XEnvD/onc0= +github.com/mailgun/mailgun-go/v5 v5.8.0 h1:yWWCD7WdYu3/VzQIKkzEiOrEC5icwwgkWKpiDZlDhqU= +github.com/mailgun/mailgun-go/v5 v5.8.0/go.mod h1:qNTXXuJi9/myqpDLI8Mbn54WCXdto1kEHm6I2/WWYQQ= github.com/mailjet/mailjet-apiv3-go/v3 v3.2.0 h1:/gjowTurgK4iqLzVAQmjtcldyaW6tbJNA4PzZsuj2Ks= github.com/mailjet/mailjet-apiv3-go/v3 v3.2.0/go.mod h1:Nw3mVzRxV0CVDTlzaRcADGKt4PMNbT7gYIyEtjMrVIM= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= @@ -49,6 +55,8 @@ github.com/modfin/henry v1.0.1 h1:PWMYC0DM4wOmyL5XxKRldKJX9qJQ2vRw+1wgLNCWLng= github.com/modfin/henry v1.0.1/go.mod h1:i8Fu1UVoYV8cHZ3mIjIXqcJBLVyuEE8pek/1UuO8PnU= 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/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= +github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -67,6 +75,8 @@ github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekuei github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1GjJohAA0p6hVEaDtHWWs= github.com/sendgrid/sendgrid-go v3.16.1+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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/internal/svc/retry.go b/internal/svc/retry.go index 7f61037..356f024 100644 --- a/internal/svc/retry.go +++ b/internal/svc/retry.go @@ -3,9 +3,9 @@ package svc import ( "context" "errors" - "fmt" "github.com/modfin/mmailer" + "github.com/modfin/mmailer/internal/logger" ) func RetryEach(ctx context.Context, s mmailer.Service, e mmailer.Email, services []mmailer.Service) (res []mmailer.Response, err error) { @@ -14,15 +14,18 @@ func RetryEach(ctx context.Context, s mmailer.Service, e mmailer.Email, services return res, nil } - var acc string = err.Error() + var errs []error for _, ss := range services { + ctx := logger.AddToLogContext(ctx, "fallback_service", ss.Name()) + logger.WarnCtx(ctx, "err sending mail, retrying with fallback", "error", err) + ctx = logger.AddToLogContext(ctx, "service", ss.Name()) res, err = ss.Send(ctx, e) if err == nil { return res, nil } - acc = fmt.Sprintf("%s: %s", err.Error(), acc) + errs = append(errs, err) } - return nil, errors.New(acc) + return nil, errors.Join(err) } func RetryOneOther(ctx context.Context, s mmailer.Service, e mmailer.Email, services []mmailer.Service) (res []mmailer.Response, err error) { @@ -34,6 +37,9 @@ func RetryOneOther(ctx context.Context, s mmailer.Service, e mmailer.Email, serv if s.Name() == ss.Name() { continue } + ctx := logger.AddToLogContext(ctx, "fallback_service", ss.Name()) + logger.WarnCtx(ctx, "err sending mail, retrying with fallback", "error", err) + ctx = logger.AddToLogContext(ctx, "service", ss.Name()) return ss.Send(ctx, e) } return nil, err diff --git a/mmailer.go b/mmailer.go index bfb34b5..b3bd697 100644 --- a/mmailer.go +++ b/mmailer.go @@ -3,11 +3,9 @@ package mmailer import ( "context" "errors" - "fmt" "io/ioutil" "net/http" "strings" - "time" "github.com/modfin/henry/slicez" "github.com/modfin/mmailer/internal/logger" @@ -68,8 +66,13 @@ func (f *Facade) Send(ctx context.Context, email Email, preferredService string) retry = RetryNone } + to := slicez.Map(email.To, func(a Address) string { + return a.Email + }) + ctx = logger.AddToLogContext(ctx, "service", service.Name()) - logger.InfoCtx(ctx, fmt.Sprintf("Sending mail to %v through %s at [%v]", email.To, service.Name(), time.Now().String())) + ctx = logger.AddToLogContext(ctx, "addresses", to) + logger.InfoCtx(ctx, "sending mail") return retry(ctx, service, email, services) } diff --git a/service.go b/service.go index cfb76aa..8120b06 100644 --- a/service.go +++ b/service.go @@ -32,11 +32,10 @@ func KeyByEmailDomain(apiKeys []ApiKey, emailFrom string) (ApiKey, bool) { domain := "" if from, err := mail.ParseAddress(emailFrom); err == nil { parts := strings.Split(from.Address, "@") - if len(parts) == 2 { - d := strings.ToLower(strings.TrimSpace(parts[1])) - if d != "" { - domain = d - } + d, _ := slicez.Last(parts) + d = strings.ToLower(strings.TrimSpace(d)) + if d != "" { + domain = d } } domainKey, ok := slicez.Find(apiKeys, func(e ApiKey) bool { diff --git a/services/mailgun/mailgun.go b/services/mailgun/mailgun.go new file mode 100644 index 0000000..a789716 --- /dev/null +++ b/services/mailgun/mailgun.go @@ -0,0 +1,254 @@ +package mailgun + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "net/mail" + "strings" + "time" + + jsoniter "github.com/json-iterator/go" + "github.com/mailgun/mailgun-go/v5" + "github.com/mailgun/mailgun-go/v5/events" + "github.com/mailgun/mailgun-go/v5/mtypes" + "github.com/modfin/henry/slicez" + "github.com/modfin/mmailer" + "github.com/modfin/mmailer/internal/logger" + "github.com/modfin/mmailer/services" +) + +type Mailgun struct { + apiKeys []mmailer.ApiKey + webhookSigningKey string + confer services.Configurer[*mailgun.PlainMessage] +} + +func New(apiKeys []mmailer.ApiKey, webhookSigningKey string) *Mailgun { + mg := &Mailgun{ + apiKeys: apiKeys, + webhookSigningKey: webhookSigningKey, + confer: configurer{}, + } + return mg +} + +func (m *Mailgun) Name() string { + return "mailgun" +} + +func (m *Mailgun) CanSend(e mmailer.Email) bool { + for _, a := range e.Attachments { + // TODO Can't find the option to set attachment content type in the mailgun api, can it be fixed? + if a.ContentType != "" && a.ContentType != "application/octet-stream" { + logger.Warn( + "mailgun: unsupported attachment content-type", + "email", e.To, + "content_type", a.ContentType, + "filename", a.Name, + ) + return false + } + } + _, ok := mmailer.KeyByEmailDomain(m.apiKeys, e.From.Email) + return ok +} + +func (m *Mailgun) newClient(addr string) (*mailgun.Client, error) { + k, ok := mmailer.KeyByEmailDomain(m.apiKeys, addr) + if !ok { + return nil, errors.New("mailgun: no api key found for " + addr) + } + client := mailgun.NewMailgun(k.Key) + if k.Props != nil && k.Props["region"] == "eu" { + err := client.SetAPIBase(mailgun.APIBaseEU) + if err != nil { + return nil, fmt.Errorf("failed to set EU region") + } + } + return client, nil +} + +func (m *Mailgun) Send(ctx context.Context, e mmailer.Email) ([]mmailer.Response, error) { + from, err := mail.ParseAddress(e.From.String()) + if err != nil { + return nil, fmt.Errorf("mailgun: failed to parse email: %w", err) + } + client, err := m.newClient(from.Address) + if err != nil { + return nil, fmt.Errorf("mailgun: failed to create client: %w", err) + } + to := slicez.Map(e.To, func(a mmailer.Address) string { + return a.String() + }) + parts := strings.Split(from.Address, "@") + domain, _ := slicez.Last(parts) + if domain == "" { + return nil, fmt.Errorf("mailgun: failed to get email domain: %v", from.Address) + } + msg := mailgun.NewMessage(domain, from.String(), e.Subject, e.Text, to...) + services.ApplyConfig(m.Name(), e.ServiceConfig, m.confer, msg) + + for _, cc := range e.Cc { + msg.AddCC(cc.String()) + } + for k, v := range e.Headers { + msg.AddHeader(k, v) + } + if strings.TrimSpace(e.Html) != "" { + msg.SetHTML(e.Html) + } + + for _, a := range e.Attachments { + b, err := base64.StdEncoding.DecodeString(a.Content) + if err != nil { + return nil, fmt.Errorf("mailgun: failed to decode attachment: %w", err) + } + msg.AddBufferAttachment(a.Name, b) + } + + resp, err := client.Send(ctx, msg) + if err != nil { + return nil, fmt.Errorf("mailgun: failed to send email: %w", err) + } + if resp.ID == "" { + return nil, fmt.Errorf("mailgun: failed to send email: %s", resp.Message) + } + return []mmailer.Response{ + { + Service: m.Name(), + + // We get raw Message-Id header here, ex <1761578515891502624.8555910852031586141@strictlog.modfin.se> + // but in the webhook, the MessageID field doesn't contain the angle brackets. + MessageId: strings.Trim(resp.ID, "<>"), + }, + }, nil +} + +func (m *Mailgun) UnmarshalPosthook(body []byte) ([]mmailer.Posthook, error) { + var webhook mtypes.WebhookPayload + if err := jsoniter.Unmarshal(body, &webhook); err != nil { + return nil, err + } + client := mailgun.NewMailgun("") // api key is not used for VerifyWebhookSignature + client.SetWebhookSigningKey(m.webhookSigningKey) + verified, err := client.VerifyWebhookSignature(webhook.Signature) + if err != nil { + return nil, fmt.Errorf("mailgun: failed to verify signature: %w", err) + } + if !verified { + return nil, errors.New("mailgun: failed to verify signature") + } + event, err := events.ParseEvent(webhook.EventData) + if err != nil { + return nil, fmt.Errorf("mailgun: failed to parse event: %w", err) + } + + b, _ := jsoniter.MarshalIndent(event, "", " ") + fmt.Println(string(b)) + + h := mmailer.Posthook{ + Service: m.Name(), + EventId: event.GetID(), + Timestamp: event.GetTimestamp(), + } + + switch e := event.(type) { + case *events.Accepted: + h.Event = mmailer.EventProcessed + h.MessageId = e.Message.Headers.MessageID + h.Email = e.Recipient + + case *events.Delivered: + h.Event = mmailer.EventDelivered + h.MessageId = e.Message.Headers.MessageID + h.Email = e.Recipient + h.Info = infoString(false, "", "", e.DeliveryStatus) + + case *events.Opened: + h.Event = mmailer.EventOpen + h.MessageId = e.Message.Headers.MessageID + h.Email = e.Recipient + + case *events.Failed: + switch e.Severity { + case "permanent": + h.Event = mmailer.EventBounce + if e.Reason == "suppress-bounce" { + h.Event = mmailer.EventDropped + } + case "temporary": + h.Event = mmailer.EventDeferred + } + h.MessageId = e.Message.Headers.MessageID + h.Email = e.Recipient + h.Info = infoString(true, e.Reason, e.Severity, e.DeliveryStatus) + + default: + logger.Warn(fmt.Sprintf("received unsupported webhook event: %s", h.Event)) + return nil, nil + } + + return []mmailer.Posthook{h}, nil +} + +func infoString(fail bool, reason, severity string, st events.DeliveryStatus) string { + latency := time.Duration(st.SessionSeconds * float64(time.Second)).Truncate(time.Millisecond) + + var words []string + if st.Code != 0 { + words = append(words, fmt.Sprintf("%d", st.Code)) + } + if st.EnhancedCode != "" { + words = append(words, st.EnhancedCode) + } + if !fail { + words = append(words, st.Message) + } + if st.MxHost != "" { + words = append(words, st.MxHost) + } + if latency > time.Millisecond { + words = append(words, latency.String()) + } + if reason != "" { + words = append(words, reason) + } + if severity != "" { + words = append(words, severity) + } + + var flags []string + if st.AttemptNo > 0 { + flags = append(flags, fmt.Sprintf("attempt:%d", st.AttemptNo)) + } + if st.Utf8 != nil && *st.Utf8 { + flags = append(flags, "utf8") + } + if st.TLS != nil && *st.TLS { + flags = append(flags, "tls") + } + if st.CertificateVerified != nil && *st.CertificateVerified { + flags = append(flags, "certificate-verified") + } + if len(flags) > 0 { + words = append(words, fmt.Sprintf("[%s]", strings.Join(flags, ", "))) + } + + msg := strings.Join(words, " ") + if fail { + msg += ": " + st.Message + } + return msg +} + +type configurer struct{} + +func (s configurer) SetIpPool(poolId string, message *mailgun.PlainMessage) { + // TODO? +} + +func (s configurer) DisableTracking(message *mailgun.PlainMessage) { + message.SetTracking(false) // untested +}