Skip to content

Commit 13437f5

Browse files
authored
Per sending domain API keys, only supports sendgrid for now (#8)
1 parent f94aa31 commit 13437f5

10 files changed

Lines changed: 148 additions & 29 deletions

File tree

cmd/mmailerd/mmailerd.go

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/labstack/echo-contrib/prometheus"
2323
"github.com/labstack/echo/v4"
2424
"github.com/labstack/echo/v4/middleware"
25+
"github.com/modfin/henry/slicez"
2526
"github.com/modfin/mmailer"
2627
"github.com/modfin/mmailer/internal/config"
2728
"github.com/modfin/mmailer/internal/logger"
@@ -202,6 +203,23 @@ func loadServices() {
202203
retry = mmailer.RetryNone
203204
}
204205

206+
domainApiKeys := make(map[string][]mmailer.ServiceApiKey)
207+
208+
for _, k := range config.Get().ServiceDomainApiKeys {
209+
parts := strings.Split(k, ":")
210+
if len(parts) != 3 {
211+
logger.Warn(fmt.Sprintf("couldn't parse row of SERVICE_DOMAIN_API_KEYS, '%s'", k))
212+
}
213+
key := mmailer.ServiceApiKey{
214+
Service: strings.ToLower(parts[0]),
215+
ApiKey: mmailer.ApiKey{
216+
Domain: strings.ToLower(parts[1]),
217+
Key: parts[2],
218+
},
219+
}
220+
domainApiKeys[key.Service] = append(domainApiKeys[key.Service], key)
221+
}
222+
205223
var services []mmailer.Service
206224
logger.Info("Services:")
207225
var weighted = strategyName == "weighted"
@@ -235,7 +253,8 @@ func loadServices() {
235253

236254
posthookUrl := fmt.Sprintf("%s/posthook?key=%s&service=%s", config.Get().PublicURL, config.Get().PosthookKey, strings.ToLower(parts[0]))
237255

238-
switch strings.ToLower(parts[0]) {
256+
service := strings.ToLower(parts[0])
257+
switch service {
239258
case "mailjet":
240259
if len(parts) != 3 {
241260
logger.Warn(fmt.Sprintf("mailjet api string is not valid, %s", s))
@@ -252,12 +271,30 @@ func loadServices() {
252271
logger.Info(fmt.Sprintf(" - Mandrill: add the following posthook url %s", posthookUrl))
253272
services = append(services, decorate(mandrill.New(parts[1])))
254273
case "sendgrid":
255-
if len(parts) != 2 {
274+
if len(parts) < 1 || len(parts) > 2 {
256275
logger.Warn("sendgrid api string is not valid,", s)
257276
continue
258277
}
278+
apiKeys := slicez.Map(domainApiKeys[service], func(k mmailer.ServiceApiKey) mmailer.ApiKey {
279+
return k.ApiKey
280+
})
281+
if len(parts) == 2 {
282+
apiKeys = append(apiKeys, mmailer.ApiKey{
283+
Domain: mmailer.ApiKeyAnyDomain,
284+
Key: parts[1],
285+
})
286+
logger.Info(" - Sendgrid: key enabled: AnyDomain")
287+
}
288+
for _, k := range domainApiKeys[service] {
289+
logger.Info(fmt.Sprintf(" - Sendgrid: key enabled: %s", k.Domain))
290+
}
291+
if len(apiKeys) == 0 {
292+
logger.Warn(" - Sendgrid: disabled, no api keys provided")
293+
continue
294+
}
295+
259296
logger.Info(fmt.Sprintf(" - Sendgrid: add the following posthook url %s", posthookUrl))
260-
services = append(services, decorate(sendgrid.New(parts[1])))
297+
services = append(services, decorate(sendgrid.New(apiKeys)))
261298
case "brev":
262299
brev, err := brev.New(parts[1:], posthookUrl)
263300
if err != nil {

internal/config/config.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package config
22

33
import (
4+
"strings"
5+
"sync"
6+
47
"github.com/caarlos0/env/v6"
58
"github.com/modfin/henry/slicez"
69
"github.com/modfin/mmailer/internal/logger"
7-
"strings"
8-
"sync"
910
)
1011

1112
type AppConfig struct {
@@ -19,8 +20,9 @@ type AppConfig struct {
1920

2021
FromDomainOverride string `env:"FROM_DOMAIN_OVERRIDE"`
2122

22-
Services []string `env:"SERVICES" envSeparator:"\n"`
23-
ServiceIpPoolConfig []string `env:"SERVICE_IP_POOL_CONFIG" envSeparator:"\n"`
23+
Services []string `env:"SERVICES" envSeparator:"\n"`
24+
ServiceIpPoolConfig []string `env:"SERVICE_IP_POOL_CONFIG" envSeparator:"\n"`
25+
ServiceDomainApiKeys []string `env:"SERVICE_DOMAIN_API_KEYS" envSeparator:"\n"`
2426

2527
RetryStrategy string `env:"RETRY_STRATEGY"`
2628
SelectStrategy string `env:"SELECT_STRATEGY"`

internal/svc/retry.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
78
"github.com/modfin/mmailer"
89
)
910

mmailer.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import (
44
"context"
55
"errors"
66
"fmt"
7-
"github.com/modfin/mmailer/internal/logger"
87
"io/ioutil"
98
"net/http"
109
"strings"
1110
"time"
11+
12+
"github.com/modfin/henry/slicez"
13+
"github.com/modfin/mmailer/internal/logger"
1214
)
1315

1416
type Facade struct {
@@ -26,7 +28,12 @@ func New(selecting SelectStrategy, retry RetryStrategy, services ...Service) *Fa
2628
}
2729

2830
func (f *Facade) Send(ctx context.Context, email Email, preferredService string) (res []Response, err error) {
29-
if len(f.Services) == 0 {
31+
32+
services := slicez.Filter(f.Services, func(s Service) bool {
33+
return s.CanSend(email)
34+
})
35+
36+
if len(services) == 0 {
3037
return nil, errors.New("facade no services to use")
3138
}
3239

@@ -35,7 +42,7 @@ func (f *Facade) Send(ctx context.Context, email Email, preferredService string)
3542
// If service is specified
3643
if len(preferredService) > 0 {
3744
preferredService = strings.ToLower(preferredService)
38-
for _, s := range f.Services {
45+
for _, s := range services {
3946
if s.Name() == preferredService {
4047
service = s
4148
break
@@ -49,7 +56,7 @@ func (f *Facade) Send(ctx context.Context, email Email, preferredService string)
4956
if strategy == nil {
5057
strategy = SelectRandom
5158
}
52-
service = strategy(f.Services)
59+
service = strategy(services)
5360
}
5461

5562
if service == nil {
@@ -63,7 +70,7 @@ func (f *Facade) Send(ctx context.Context, email Email, preferredService string)
6370

6471
ctx = logger.AddToLogContext(ctx, "service", service.Name())
6572
logger.InfoCtx(ctx, fmt.Sprintf("Sending mail to %v through %s at [%v]", email.To, service.Name(), time.Now().String()))
66-
return retry(ctx, service, email, f.Services)
73+
return retry(ctx, service, email, services)
6774
}
6875

6976
func (f *Facade) UnmarshalPosthook(r *http.Request) (res []Posthook, err error) {

service.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,49 @@ package mmailer
22

33
import (
44
"context"
5+
"net/mail"
6+
"strings"
7+
8+
"github.com/modfin/henry/slicez"
59
)
610

711
type Service interface {
812
Name() string
13+
CanSend(email Email) bool
914
Send(ctx context.Context, email Email) (res []Response, err error)
1015
UnmarshalPosthook(body []byte) ([]Posthook, error)
1116
}
17+
18+
type ServiceApiKey struct {
19+
Service string
20+
ApiKey
21+
}
22+
23+
type ApiKey struct {
24+
Domain string
25+
Key string
26+
}
27+
28+
const ApiKeyAnyDomain = ""
29+
30+
func KeyByEmailDomain(apiKeys []ApiKey, emailFrom string) (ApiKey, bool) {
31+
domain := ""
32+
if from, err := mail.ParseAddress(emailFrom); err == nil {
33+
parts := strings.Split(from.Address, "@")
34+
if len(parts) == 2 {
35+
d := strings.ToLower(strings.TrimSpace(parts[1]))
36+
if d != "" {
37+
domain = d
38+
}
39+
}
40+
}
41+
domainKey, ok := slicez.Find(apiKeys, func(e ApiKey) bool {
42+
return domain != "" && e.Domain == domain
43+
})
44+
if ok {
45+
return domainKey, true
46+
}
47+
return slicez.Find(apiKeys, func(e ApiKey) bool {
48+
return e.Domain == ApiKeyAnyDomain
49+
})
50+
}

services/brev/brev.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8+
89
"github.com/modfin/brev"
910
"github.com/modfin/mmailer"
1011
"github.com/modfin/mmailer/internal/logger"
@@ -57,6 +58,10 @@ func (b *Brev) Name() string {
5758
return "brev"
5859
}
5960

61+
func (*Brev) CanSend(email mmailer.Email) bool {
62+
return true // per domain keys not implemented
63+
}
64+
6065
func (b *Brev) Send(ctx context.Context, m mmailer.Email) (res []mmailer.Response, err error) {
6166
if b.client == nil {
6267
return nil, errors.New("brev: cant send, missing client")

services/generic/generic.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@ import (
55
"encoding/base64"
66
"errors"
77
"fmt"
8+
"net/smtp"
9+
"net/url"
10+
"os"
11+
"strings"
12+
813
"github.com/google/uuid"
914
"github.com/modfin/mmailer"
1015
"github.com/modfin/mmailer/internal/logger"
1116
"github.com/modfin/mmailer/internal/smtpx"
1217
"github.com/modfin/mmailer/services"
13-
"net/smtp"
14-
"net/url"
15-
"os"
16-
"strings"
1718
)
1819

1920
// make generic implement mmailer.Service interface by implementing Name and Send methods
@@ -36,6 +37,10 @@ func (g *Generic) Name() string {
3637
return fmt.Sprintf("Generic smtp %s", g.smtpUrl.Host)
3738
}
3839

40+
func (g *Generic) CanSend(email mmailer.Email) bool {
41+
return true // per domain keys not implemented
42+
}
43+
3944
func (g *Generic) Send(ctx context.Context, email mmailer.Email) (res []mmailer.Response, err error) {
4045
message := smtpx.NewMessage()
4146
for k, v := range email.Headers {

services/mailjet/mailjet.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"strings"
8+
79
mj "github.com/mailjet/mailjet-apiv3-go/v3"
810
"github.com/modfin/mmailer"
911
"github.com/modfin/mmailer/services"
10-
"strings"
1112
)
1213

1314
type Mailjet struct {
@@ -71,6 +72,10 @@ func (m *Mailjet) Name() string {
7172
return "mailjet"
7273
}
7374

75+
func (*Mailjet) CanSend(email mmailer.Email) bool {
76+
return true // per domain keys not implemented
77+
}
78+
7479
func (m *Mailjet) Send(_ context.Context, email mmailer.Email) (res []mmailer.Response, err error) {
7580
message := mj.InfoMessagesV31{
7681
Headers: map[string]interface{}{},

services/mandrill/mandrill.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7-
"github.com/keighl/mandrill"
8-
"github.com/modfin/mmailer"
9-
"github.com/modfin/mmailer/services"
107
"net/http"
118
"net/url"
129
"strings"
1310
"time"
11+
12+
"github.com/keighl/mandrill"
13+
"github.com/modfin/mmailer"
14+
"github.com/modfin/mmailer/services"
1415
)
1516

1617
type Mandrill struct {
@@ -37,6 +38,10 @@ func (m *Mandrill) Name() string {
3738
return "mandrill"
3839
}
3940

41+
func (*Mandrill) CanSend(email mmailer.Email) bool {
42+
return true // per domain keys not implemented
43+
}
44+
4045
func (m *Mandrill) Send(_ context.Context, email mmailer.Email) (res []mmailer.Response, err error) {
4146
message := &mandrill.Message{}
4247

services/sendgrid/sendgrid.go

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package sendgrid
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"strings"
89

@@ -16,25 +17,34 @@ import (
1617
)
1718

1819
type Sendgrid struct {
19-
apiKey string
20-
confer services.Configurer[*mail.SGMailV3]
20+
apiKeys []mmailer.ApiKey
21+
confer services.Configurer[*mail.SGMailV3]
2122
}
2223

23-
func (m *Sendgrid) newClient() *sendgrid.Client {
24-
return sendgrid.NewSendClient(m.apiKey)
24+
func (m *Sendgrid) newClient(addr string) (*sendgrid.Client, error) {
25+
k, ok := mmailer.KeyByEmailDomain(m.apiKeys, addr)
26+
if !ok {
27+
return nil, errors.New("sendgrid: no api key found for " + addr)
28+
}
29+
return sendgrid.NewSendClient(k.Key), nil
2530
}
2631

27-
func New(apiKey string) *Sendgrid {
32+
func New(apiKeys []mmailer.ApiKey) *Sendgrid {
2833
return &Sendgrid{
29-
apiKey: apiKey,
30-
confer: SendgridConfigurer{},
34+
apiKeys: apiKeys,
35+
confer: SendgridConfigurer{},
3136
}
3237
}
3338

3439
func (m *Sendgrid) Name() string {
3540
return "sendgrid"
3641
}
3742

43+
func (m *Sendgrid) CanSend(email mmailer.Email) bool {
44+
_, ok := mmailer.KeyByEmailDomain(m.apiKeys, email.From.Email)
45+
return ok
46+
}
47+
3848
func (m *Sendgrid) Send(_ context.Context, email mmailer.Email) (res []mmailer.Response, err error) {
3949
from := mail.NewEmail(email.From.Name, email.From.Email)
4050

@@ -78,8 +88,11 @@ func (m *Sendgrid) Send(_ context.Context, email mmailer.Email) (res []mmailer.R
7888
Address: a.Email,
7989
})
8090
}
81-
82-
response, err := m.newClient().Send(message)
91+
client, err := m.newClient(email.From.Email)
92+
if err != nil {
93+
return nil, err
94+
}
95+
response, err := client.Send(message)
8396
if err != nil {
8497
return nil, fmt.Errorf("%s: %s", m.Name(), err)
8598
}

0 commit comments

Comments
 (0)