From 3a1d4af1d0e4a0b38b9cd70581cf3178945f280a Mon Sep 17 00:00:00 2001 From: MFMarkus Date: Mon, 22 Sep 2025 11:04:37 +0200 Subject: [PATCH 1/4] Mailgin - wip --- .depot.cache.json | 2 +- LICENSES_DEP | 8 ++++++-- go.mod | 4 ++++ go.sum | 14 ++++++++++++++ services/mailgun/mailgun.go | 38 +++++++++++++++++++++++++++++++++++++ 5 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 services/mailgun/mailgun.go 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/go.mod b/go.mod index 780d278..d9f311f 100644 --- a/go.mod +++ b/go.mod @@ -22,8 +22,11 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gobuffalo/envy v1.10.2 // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect + github.com/joho/godotenv v1.4.0 // indirect github.com/labstack/gommon v0.4.2 // indirect + github.com/mailgun/mailgun-go v2.0.0+incompatible // 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 @@ -33,6 +36,7 @@ require ( github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.0 // indirect github.com/prometheus/procfs v0.17.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect diff --git a/go.sum b/go.sum index 9c9dd3c..91fd497 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/gobuffalo/envy v1.10.2 h1:EIi03p9c3yeuRCFPOKcSfajzkLb3hrRjEpHGI8I2Wo4= +github.com/gobuffalo/envy v1.10.2/go.mod h1:qGAGwdvDsaEtPhfBzb3o0SfDea8ByGn9j8bKmVft9z8= 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= @@ -16,6 +18,8 @@ github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrR github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/keighl/mandrill v0.0.0-20170605120353-1775dd4b3b41 h1:UKfGHZTAF2kEH9TkM4aw+IxpjiNXzB/c8+/N2yUQf+c= github.com/keighl/mandrill v0.0.0-20170605120353-1775dd4b3b41/go.mod h1:+mEDstlvUwlcUrduEzwxRm8q3GnjOa7GlNVJdgvWpiU= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -32,6 +36,8 @@ 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/mailgun-go v2.0.0+incompatible h1:0FoRHWwMUctnd8KIR3vtZbqdfjpIMxOZgcSa51s8F8o= +github.com/mailgun/mailgun-go v2.0.0+incompatible/go.mod h1:NWTyU+O4aczg/nsGhQnvHL6v2n5Gy6Sv5tNDVvC6FbU= 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= @@ -51,6 +57,9 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +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.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= @@ -61,6 +70,7 @@ github.com/prometheus/common v0.66.0 h1:K/rJPHrG3+AoQs50r2+0t7zMnMzek2Vbv31OFVsM github.com/prometheus/common v0.66.0/go.mod h1:Ux6NtV1B4LatamKE63tJBntoxD++xmtI/lK0VtEplN4= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= @@ -68,9 +78,12 @@ github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 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= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -97,5 +110,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/services/mailgun/mailgun.go b/services/mailgun/mailgun.go new file mode 100644 index 0000000..98c46e2 --- /dev/null +++ b/services/mailgun/mailgun.go @@ -0,0 +1,38 @@ +package mailgun + +import ( + "context" + "github.com/mailgun/mailgun-go" + "github.com/modfin/mmailer" +) + +type Mailgun struct { + client *mailgun.MailgunImpl +} + +func New(domain, apiKey, posthookUrl string) (*Mailgun, error) { + mg := &Mailgun{ + client: mailgun.NewMailgun(domain, apiKey), + } + // TODO: provide some unique id here, like mmailer-slog for slog deployment? + err := mg.client.CreateWebhook("mmailer-id", posthookUrl) + if err != nil { + return nil, err + } + return mg, nil +} + +func (m *Mailgun) Name() string { + return "mailgun" +} + +func (m *Mailgun) Send(_ context.Context, email mmailer.Email) ([]mmailer.Response, error) { + // TODO + return nil, nil +} + +func (m *Mailgun) UnmarshalPosthook(body []byte) ([]mmailer.Posthook, error) { + // TODO + // mailgun.Event{} + return nil, nil +} From 4810e31b1312ef002c6e38da337b6a2600cb2337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joel=20Edstr=C3=B6m?= Date: Mon, 27 Oct 2025 16:52:46 +0100 Subject: [PATCH 2/4] Kind of working, bounce event untested --- cmd/mmailerd/mmailerd.go | 8 ++ go.mod | 7 +- go.sum | 24 ++--- services/mailgun/mailgun.go | 180 ++++++++++++++++++++++++++++++++---- 4 files changed, 185 insertions(+), 34 deletions(-) diff --git a/cmd/mmailerd/mmailerd.go b/cmd/mmailerd/mmailerd.go index 1bb92a1..0f3d8ea 100644 --- a/cmd/mmailerd/mmailerd.go +++ b/cmd/mmailerd/mmailerd.go @@ -29,6 +29,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" @@ -294,6 +295,13 @@ 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) != 3 { + logger.Warn("mailgun api string is not valid,", s) + continue + } + logger.Info(fmt.Sprintf(" - Mailgun: add the following posthook url %s", posthookUrl)) + services = append(services, decorate(mailgun.New(parts[1], parts[2]))) 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 d9f311f..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 @@ -22,21 +23,19 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gobuffalo/envy v1.10.2 // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect - github.com/joho/godotenv v1.4.0 // indirect github.com/labstack/gommon v0.4.2 // indirect - github.com/mailgun/mailgun-go v2.0.0+incompatible // 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 github.com/prometheus/procfs v0.17.0 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect diff --git a/go.sum b/go.sum index 91fd497..60516c6 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +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/gobuffalo/envy v1.10.2 h1:EIi03p9c3yeuRCFPOKcSfajzkLb3hrRjEpHGI8I2Wo4= -github.com/gobuffalo/envy v1.10.2/go.mod h1:qGAGwdvDsaEtPhfBzb3o0SfDea8ByGn9j8bKmVft9z8= +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= @@ -18,8 +18,6 @@ github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrR github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/keighl/mandrill v0.0.0-20170605120353-1775dd4b3b41 h1:UKfGHZTAF2kEH9TkM4aw+IxpjiNXzB/c8+/N2yUQf+c= github.com/keighl/mandrill v0.0.0-20170605120353-1775dd4b3b41/go.mod h1:+mEDstlvUwlcUrduEzwxRm8q3GnjOa7GlNVJdgvWpiU= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -36,8 +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/mailgun-go v2.0.0+incompatible h1:0FoRHWwMUctnd8KIR3vtZbqdfjpIMxOZgcSa51s8F8o= -github.com/mailgun/mailgun-go v2.0.0+incompatible/go.mod h1:NWTyU+O4aczg/nsGhQnvHL6v2n5Gy6Sv5tNDVvC6FbU= +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= @@ -55,11 +55,10 @@ 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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -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.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= @@ -70,20 +69,18 @@ github.com/prometheus/common v0.66.0 h1:K/rJPHrG3+AoQs50r2+0t7zMnMzek2Vbv31OFVsM github.com/prometheus/common v0.66.0/go.mod h1:Ux6NtV1B4LatamKE63tJBntoxD++xmtI/lK0VtEplN4= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= github.com/sendgrid/sendgrid-go v3.16.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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 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= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -110,6 +107,5 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/services/mailgun/mailgun.go b/services/mailgun/mailgun.go index 98c46e2..6e5a754 100644 --- a/services/mailgun/mailgun.go +++ b/services/mailgun/mailgun.go @@ -2,37 +2,185 @@ package mailgun import ( "context" - "github.com/mailgun/mailgun-go" + "encoding/base64" + "errors" + "fmt" + "net/mail" + "strings" + + 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 { - client *mailgun.MailgunImpl + client *mailgun.Client + confer services.Configurer[*mailgun.PlainMessage] } -func New(domain, apiKey, posthookUrl string) (*Mailgun, error) { +func New(apiKey string, webhookSigningKey string) *Mailgun { mg := &Mailgun{ - client: mailgun.NewMailgun(domain, apiKey), - } - // TODO: provide some unique id here, like mmailer-slog for slog deployment? - err := mg.client.CreateWebhook("mmailer-id", posthookUrl) - if err != nil { - return nil, err + client: mailgun.NewMailgun(apiKey), + confer: configurer{}, } - return mg, nil + mg.client.SetWebhookSigningKey(webhookSigningKey) + _ = mg.client.SetAPIBase(mailgun.APIBaseEU) + return mg } func (m *Mailgun) Name() string { return "mailgun" } -func (m *Mailgun) Send(_ context.Context, email mmailer.Email) ([]mmailer.Response, error) { - // TODO - return nil, nil +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 + } + } + if !strings.HasSuffix(e.From.Email, "strictlog.modfin.se") { + return false + } + return true +} + +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) + } + parts := strings.Split(from.Address, "@") + domain, _ := slicez.Last(parts) + if domain == "" { + return nil, fmt.Errorf("mailgun: failed to get email domain: %v", from.Address) + } + + to := slicez.Map(e.To, func(a mmailer.Address) string { + return a.String() + }) + + 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 := m.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) { - // TODO - // mailgun.Event{} - return nil, nil + var webhook mtypes.WebhookPayload + if err := jsoniter.Unmarshal(body, &webhook); err != nil { + return nil, err + } + verified, err := m.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 + + 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 + + case "temporary": + h.Event = mmailer.EventDeferred + if e.Reason == "suppress-bounce" { + h.Event = mmailer.EventDropped + } + } + + h.MessageId = e.Message.Headers.MessageID + h.Email = e.Recipient + h.Info = fmt.Sprintf("%s; %d; %s", e.Reason, e.DeliveryStatus.Code, e.DeliveryStatus.Description) + default: + logger.Warn(fmt.Sprintf("received unsupported webhook event: %s", h.Event)) + return nil, nil + } + + return []mmailer.Posthook{h}, nil +} + +type configurer struct{} + +func (s configurer) SetIpPool(poolId string, message *mailgun.PlainMessage) { + // TODO? +} + +func (s configurer) DisableTracking(message *mailgun.PlainMessage) { + message.SetTracking(false) } From cbe0782b7fa324865dc78b525a0698c8f712731e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joel=20Edstr=C3=B6m?= Date: Tue, 28 Oct 2025 17:41:56 +0100 Subject: [PATCH 3/4] Improve delivery/bounce info --- cmd/mmailerd/mmailerd.go | 25 ++++++++- service.go | 9 ++- services/mailgun/mailgun.go | 107 +++++++++++++++++++++++++++++------- 3 files changed, 115 insertions(+), 26 deletions(-) diff --git a/cmd/mmailerd/mmailerd.go b/cmd/mmailerd/mmailerd.go index 0f3d8ea..a27805a 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" @@ -296,12 +297,32 @@ 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) != 3 { + 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(parts[1], parts[2]))) + 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/service.go b/service.go index cfb76aa..4c5733d 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 - } + domain, _ := slicez.Last(parts) + d := strings.ToLower(strings.TrimSpace(domain)) + 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 index 6e5a754..ec4b894 100644 --- a/services/mailgun/mailgun.go +++ b/services/mailgun/mailgun.go @@ -7,6 +7,7 @@ import ( "fmt" "net/mail" "strings" + "time" jsoniter "github.com/json-iterator/go" "github.com/mailgun/mailgun-go/v5" @@ -19,17 +20,17 @@ import ( ) type Mailgun struct { - client *mailgun.Client - confer services.Configurer[*mailgun.PlainMessage] + apiKeys []mmailer.ApiKey + webhookSigningKey string + confer services.Configurer[*mailgun.PlainMessage] } -func New(apiKey string, webhookSigningKey string) *Mailgun { +func New(apiKeys []mmailer.ApiKey, webhookSigningKey string) *Mailgun { mg := &Mailgun{ - client: mailgun.NewMailgun(apiKey), - confer: configurer{}, + apiKeys: apiKeys, + webhookSigningKey: webhookSigningKey, + confer: configurer{}, } - mg.client.SetWebhookSigningKey(webhookSigningKey) - _ = mg.client.SetAPIBase(mailgun.APIBaseEU) return mg } @@ -50,10 +51,23 @@ func (m *Mailgun) CanSend(e mmailer.Email) bool { return false } } - if !strings.HasSuffix(e.From.Email, "strictlog.modfin.se") { - 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("sendgrid: 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 true + return client, nil } func (m *Mailgun) Send(ctx context.Context, e mmailer.Email) ([]mmailer.Response, error) { @@ -66,7 +80,10 @@ func (m *Mailgun) Send(ctx context.Context, e mmailer.Email) ([]mmailer.Response if domain == "" { return nil, fmt.Errorf("mailgun: failed to get email domain: %v", from.Address) } - + client, err := m.newClient(domain) + 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() }) @@ -92,7 +109,7 @@ func (m *Mailgun) Send(ctx context.Context, e mmailer.Email) ([]mmailer.Response msg.AddBufferAttachment(a.Name, b) } - resp, err := m.client.Send(ctx, msg) + resp, err := client.Send(ctx, msg) if err != nil { return nil, fmt.Errorf("mailgun: failed to send email: %w", err) } @@ -115,7 +132,9 @@ func (m *Mailgun) UnmarshalPosthook(body []byte) ([]mmailer.Posthook, error) { if err := jsoniter.Unmarshal(body, &webhook); err != nil { return nil, err } - verified, err := m.client.VerifyWebhookSignature(webhook.Signature) + 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) } @@ -146,6 +165,7 @@ func (m *Mailgun) UnmarshalPosthook(body []byte) ([]mmailer.Posthook, error) { 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 @@ -156,17 +176,16 @@ func (m *Mailgun) UnmarshalPosthook(body []byte) ([]mmailer.Posthook, error) { switch e.Severity { case "permanent": h.Event = mmailer.EventBounce - - case "temporary": - h.Event = mmailer.EventDeferred 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 = fmt.Sprintf("%s; %d; %s", e.Reason, e.DeliveryStatus.Code, e.DeliveryStatus.Description) + 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 @@ -175,6 +194,56 @@ func (m *Mailgun) UnmarshalPosthook(body []byte) ([]mmailer.Posthook, error) { 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) { @@ -182,5 +251,5 @@ func (s configurer) SetIpPool(poolId string, message *mailgun.PlainMessage) { } func (s configurer) DisableTracking(message *mailgun.PlainMessage) { - message.SetTracking(false) + message.SetTracking(false) // untested } From 72ea55dab89a5fb597737df59800e228e3cff6db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joel=20Edstr=C3=B6m?= Date: Thu, 30 Oct 2025 10:26:17 +0100 Subject: [PATCH 4/4] Fixes --- cmd/mmailerd/mmailerd.go | 6 +++--- internal/svc/retry.go | 14 ++++++++++---- mmailer.go | 9 ++++++--- service.go | 4 ++-- services/mailgun/mailgun.go | 17 ++++++++--------- 5 files changed, 29 insertions(+), 21 deletions(-) diff --git a/cmd/mmailerd/mmailerd.go b/cmd/mmailerd/mmailerd.go index a27805a..3e05dff 100644 --- a/cmd/mmailerd/mmailerd.go +++ b/cmd/mmailerd/mmailerd.go @@ -91,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") 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 4c5733d..8120b06 100644 --- a/service.go +++ b/service.go @@ -32,8 +32,8 @@ func KeyByEmailDomain(apiKeys []ApiKey, emailFrom string) (ApiKey, bool) { domain := "" if from, err := mail.ParseAddress(emailFrom); err == nil { parts := strings.Split(from.Address, "@") - domain, _ := slicez.Last(parts) - d := strings.ToLower(strings.TrimSpace(domain)) + d, _ := slicez.Last(parts) + d = strings.ToLower(strings.TrimSpace(d)) if d != "" { domain = d } diff --git a/services/mailgun/mailgun.go b/services/mailgun/mailgun.go index ec4b894..a789716 100644 --- a/services/mailgun/mailgun.go +++ b/services/mailgun/mailgun.go @@ -41,7 +41,7 @@ func (m *Mailgun) Name() string { 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" { + if a.ContentType != "" && a.ContentType != "application/octet-stream" { logger.Warn( "mailgun: unsupported attachment content-type", "email", e.To, @@ -58,7 +58,7 @@ func (m *Mailgun) CanSend(e mmailer.Email) bool { func (m *Mailgun) newClient(addr string) (*mailgun.Client, error) { k, ok := mmailer.KeyByEmailDomain(m.apiKeys, addr) if !ok { - return nil, errors.New("sendgrid: no api key found for " + addr) + return nil, errors.New("mailgun: no api key found for " + addr) } client := mailgun.NewMailgun(k.Key) if k.Props != nil && k.Props["region"] == "eu" { @@ -75,19 +75,18 @@ func (m *Mailgun) Send(ctx context.Context, e mmailer.Email) ([]mmailer.Response if err != nil { return nil, fmt.Errorf("mailgun: failed to parse email: %w", err) } - parts := strings.Split(from.Address, "@") - domain, _ := slicez.Last(parts) - if domain == "" { - return nil, fmt.Errorf("mailgun: failed to get email domain: %v", from.Address) - } - client, err := m.newClient(domain) + 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)