Skip to content

Commit f902186

Browse files
authored
enhancement: Added the ability to configure a list of domains and only send mail to addresses in those domains (#21)
* enhancement: Added the ability to configure a list of domains and only send mail to addresses in those domains * Added env var docs * Updated helm chart
1 parent 687146e commit f902186

File tree

8 files changed

+106
-14
lines changed

8 files changed

+106
-14
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@ make docker
2121
make docker-run
2222
```
2323

24+
### Configuration
25+
A number of environment variables are available to configure the service at runtime:
26+
| Env var name | Functionality | Default |
27+
|--------------|---------------|---------|
28+
| SERVICE_PORT | The local port the application will bind to | 80 |
29+
| SENDGRID_API_KEY | The API Key for sendgrid | |
30+
| SLACK_API_KEY | The API Token for Slack | |
31+
| GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS | The number of seconds the application will continue servicing in-flight requests before the application stops after it receives an interrupt signal | 10 |
32+
| STRUCTURED_LOGGING | If enabled, logs will be in JSON format, and only above INFO level | false |
33+
| ALLOW_EMAIL_TO_DOMAINS | A comma separated list of domains. Only addresses in this list can have email sent to them. If empty, disable this "sandboxing" functionality. | |
34+
35+
2436
### Releasing a new version on GitHub and Brew
2537

2638
We are using a tool called `goreleaser` which you can get from brew if you're on MacOS:

charts/zero-notifcation-service/Chart.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ type: application
1515
# This is the chart version. This version number should be incremented each time you make changes
1616
# to the chart and its templates, including the app version.
1717
# Versions are expected to follow Semantic Versioning (https://semver.org/)
18-
version: 0.0.5
18+
version: 0.0.6
1919

2020
# This is the version number of the application being deployed. This version number should be
2121
# incremented each time you make changes to the application. Versions are not expected to
2222
# follow Semantic Versioning. They should reflect the version the application is using.
23-
appVersion: 0.0.5
23+
appVersion: 0.0.9

charts/zero-notifcation-service/templates/configmap.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ data:
66
SERVICE_PORT: "{{ .Values.service.port }}"
77
GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS: "{{ .Values.application.gracefulShutdownTimeout }}"
88
STRUCTURED_LOGGING: "{{ .Values.application.structuredLogging }}"
9+
ALLOW_EMAIL_TO_DOMAINS: "{{ .Values.application.allowEmailToDomains }}"

charts/zero-notifcation-service/values.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,11 @@ affinity: {}
7676

7777

7878
# Application Config
79+
# See project readme for more information about config options
7980

8081
application:
8182
sendgridApiKey:
8283
slackApiKey:
8384
gracefulShutdownTimeout: 10
8485
structuredLogging: true
86+
allowEmailToDomains:

internal/config/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type Config struct {
1414
GracefulShutdownTimeout time.Duration
1515
StructuredLogging bool
1616
DebugDumpRequests bool
17+
AllowEmailToDomains []string
1718
}
1819

1920
var config *Config
@@ -26,6 +27,7 @@ const (
2627
GracefulShutdownTimeout
2728
StructuredLogging
2829
DebugDumpRequests
30+
AllowEmailToDomains
2931
)
3032

3133
// GetConfig returns a pointer to the singleton Config object
@@ -55,13 +57,17 @@ func loadConfig() *Config {
5557
viper.SetDefault(DebugDumpRequests, "false")
5658
viper.BindEnv(DebugDumpRequests, "DEBUG_DUMP_REQUESTS")
5759

60+
viper.SetDefault(AllowEmailToDomains, []string{})
61+
viper.BindEnv(AllowEmailToDomains, "ALLOW_EMAIL_TO_DOMAINS")
62+
5863
config := Config{
5964
Port: viper.GetInt(Port),
6065
SendgridAPIKey: viper.GetString(SendgridAPIKey),
6166
SlackAPIKey: viper.GetString(SlackAPIKey),
6267
GracefulShutdownTimeout: viper.GetDuration(GracefulShutdownTimeout),
6368
StructuredLogging: viper.GetBool(StructuredLogging),
6469
DebugDumpRequests: viper.GetBool(DebugDumpRequests),
70+
AllowEmailToDomains: viper.GetStringSlice(AllowEmailToDomains),
6571
}
6672

6773
return &config

internal/mail/mail.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package mail
22

33
import (
4+
"fmt"
5+
"strings"
46
"sync"
57

68
"github.com/commitdev/zero-notification-service/internal/server"
@@ -81,3 +83,24 @@ func convertAddresses(addresses []server.EmailRecipient) []*sendgridMail.Email {
8183
}
8284
return returnAddresses
8385
}
86+
87+
// RemoveInvalidRecipients accepts a list of recipients and removes the ones with domains not in the allowed list
88+
func RemoveInvalidRecipients(recipients []server.EmailRecipient, allowedDomains []string) []server.EmailRecipient {
89+
valid := []server.EmailRecipient{}
90+
for _, recipient := range recipients {
91+
if addressInAllowedDomain(recipient.Address, allowedDomains) {
92+
valid = append(valid, recipient)
93+
}
94+
}
95+
return valid
96+
}
97+
98+
// addressInAllowedDomain checks if a single email address is in a list of domains
99+
func addressInAllowedDomain(address string, domains []string) bool {
100+
for _, domain := range domains {
101+
if strings.HasSuffix(address, fmt.Sprintf("@%s", domain)) {
102+
return true
103+
}
104+
}
105+
return false
106+
}

internal/mail/mail_test.go

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,7 @@ func (cl *FakeClient) Send(email *sendgridMail.SGMailV3) (*rest.Response, error)
2525
}
2626

2727
func TestSendBulkMail(t *testing.T) {
28-
var toList []server.EmailRecipient
29-
// Create a random number of mails
30-
rand.Seed(time.Now().UnixNano())
31-
sendCount := rand.Intn(5) + 2
32-
for i := 0; i < sendCount; i++ {
33-
toList = append(toList, server.EmailRecipient{
34-
Name: fmt.Sprintf("Test Recipient %d", i),
35-
Address: fmt.Sprintf("address%[email protected]", i),
36-
})
37-
}
28+
toList := createRandomRecipients(2, 5)
3829
cc := make([]server.EmailRecipient, 0)
3930
bcc := make([]server.EmailRecipient, 0)
4031
from := server.EmailSender{Name: "Test User", Address: "[email protected]"}
@@ -52,8 +43,37 @@ func TestSendBulkMail(t *testing.T) {
5243
returnedCount++
5344
}
5445

55-
assert.Equal(t, sendCount, returnedCount, "Response count should match requests sent")
46+
assert.Equal(t, len(toList), returnedCount, "Response count should match requests sent")
5647

5748
// Check that the send function was called
58-
client.AssertNumberOfCalls(t, "Send", sendCount)
49+
client.AssertNumberOfCalls(t, "Send", len(toList))
50+
}
51+
52+
func TestRemoveInvalidRecipients(t *testing.T) {
53+
toList := createRandomRecipients(2, 5)
54+
55+
originalSize := len(toList)
56+
57+
toList[0].Address = "[email protected]"
58+
59+
alteredList := mail.RemoveInvalidRecipients(toList, []string{"commit.dev", "domain.com"})
60+
assert.Equal(t, len(alteredList), originalSize, "All addresses should remain in the list")
61+
62+
alteredList = mail.RemoveInvalidRecipients(toList, []string{"commit.dev"})
63+
assert.Equal(t, len(alteredList), 1, "1 address should remain in the list")
64+
}
65+
66+
// createRandomRecipients creates a random list of recipients
67+
func createRandomRecipients(min int, randCount int) []server.EmailRecipient {
68+
var toList []server.EmailRecipient
69+
// Create a random number of mails
70+
rand.Seed(time.Now().UnixNano())
71+
sendCount := rand.Intn(randCount) + min
72+
for i := 0; i < sendCount; i++ {
73+
toList = append(toList, server.EmailRecipient{
74+
Name: fmt.Sprintf("Test Recipient %d", i),
75+
Address: fmt.Sprintf("address%[email protected]", i),
76+
})
77+
}
78+
return toList
5979
}

internal/service/api_email_service.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,20 @@ func NewEmailApiService(c *config.Config) server.EmailApiServicer {
2626

2727
// SendEmail - Send an email
2828
func (s *EmailApiService) SendEmail(ctx context.Context, sendMailRequest server.SendMailRequest) (server.ImplResponse, error) {
29+
30+
// Check if there are valid recipients who are not in the restriction list
31+
if len(s.config.AllowEmailToDomains) > 0 {
32+
originalAddresses := sendMailRequest.ToAddresses
33+
sendMailRequest.ToAddresses = mail.RemoveInvalidRecipients(sendMailRequest.ToAddresses, s.config.AllowEmailToDomains)
34+
// If there are none, return with a 200 but don't send anything
35+
if len(sendMailRequest.ToAddresses) == 0 {
36+
zap.S().Infow("No valid Recipients for send", zap.Any("original_addresses", originalAddresses))
37+
return server.Response(http.StatusOK, server.SendMailResponse{TrackingId: "No valid recipients"}), nil
38+
}
39+
}
40+
sendMailRequest.CcAddresses = mail.RemoveInvalidRecipients(sendMailRequest.CcAddresses, s.config.AllowEmailToDomains)
41+
sendMailRequest.BccAddresses = mail.RemoveInvalidRecipients(sendMailRequest.BccAddresses, s.config.AllowEmailToDomains)
42+
2943
client := sendgrid.NewSendClient(s.config.SendgridAPIKey)
3044
response, err := mail.SendIndividualMail(sendMailRequest.ToAddresses, sendMailRequest.FromAddress, sendMailRequest.CcAddresses, sendMailRequest.BccAddresses, sendMailRequest.Message, client)
3145

@@ -45,6 +59,20 @@ func (s *EmailApiService) SendEmail(ctx context.Context, sendMailRequest server.
4559

4660
// SendBulk - Send a batch of emails to many users with the same content. Note that it is possible for only a subset of these to fail.
4761
func (s *EmailApiService) SendBulk(ctx context.Context, sendBulkMailRequest server.SendBulkMailRequest) (server.ImplResponse, error) {
62+
// Check if there are valid recipients who are not in the restriction list
63+
if len(s.config.AllowEmailToDomains) > 0 {
64+
originalAddresses := sendBulkMailRequest.ToAddresses
65+
sendBulkMailRequest.ToAddresses = mail.RemoveInvalidRecipients(sendBulkMailRequest.ToAddresses, s.config.AllowEmailToDomains)
66+
67+
// If there are none, return with a 200 but don't send anything
68+
if len(sendBulkMailRequest.ToAddresses) == 0 {
69+
zap.S().Infow("No valid Recipients for bulk send", zap.Any("original_addresses", originalAddresses))
70+
return server.Response(http.StatusOK, server.SendMailResponse{TrackingId: "No valid recipients"}), nil
71+
}
72+
}
73+
sendBulkMailRequest.CcAddresses = mail.RemoveInvalidRecipients(sendBulkMailRequest.CcAddresses, s.config.AllowEmailToDomains)
74+
sendBulkMailRequest.BccAddresses = mail.RemoveInvalidRecipients(sendBulkMailRequest.BccAddresses, s.config.AllowEmailToDomains)
75+
4876
client := sendgrid.NewSendClient(s.config.SendgridAPIKey)
4977

5078
responseChannel := make(chan mail.BulkSendAttempt)

0 commit comments

Comments
 (0)