Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ make docker
make docker-run
```

### Configuration
A number of environment variables are available to configure the service at runtime:
| Env var name | Functionality | Default |
|--------------|---------------|---------|
| SERVICE_PORT | The local port the application will bind to | 80 |
| SENDGRID_API_KEY | The API Key for sendgrid | |
| SLACK_API_KEY | The API Token for Slack | |
| 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 |
| STRUCTURED_LOGGING | If enabled, logs will be in JSON format, and only above INFO level | false |
| 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. | |


### Releasing a new version on GitHub and Brew

We are using a tool called `goreleaser` which you can get from brew if you're on MacOS:
Expand Down
4 changes: 2 additions & 2 deletions charts/zero-notifcation-service/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.0.5
version: 0.0.6

# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
appVersion: 0.0.5
appVersion: 0.0.9
1 change: 1 addition & 0 deletions charts/zero-notifcation-service/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ data:
SERVICE_PORT: "{{ .Values.service.port }}"
GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS: "{{ .Values.application.gracefulShutdownTimeout }}"
STRUCTURED_LOGGING: "{{ .Values.application.structuredLogging }}"
ALLOW_EMAIL_TO_DOMAINS: "{{ .Values.application.allowEmailToDomains }}"
2 changes: 2 additions & 0 deletions charts/zero-notifcation-service/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,11 @@ affinity: {}


# Application Config
# See project readme for more information about config options

application:
sendgridApiKey:
slackApiKey:
gracefulShutdownTimeout: 10
structuredLogging: true
allowEmailToDomains:
6 changes: 6 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Config struct {
GracefulShutdownTimeout time.Duration
StructuredLogging bool
DebugDumpRequests bool
AllowEmailToDomains []string
}

var config *Config
Expand All @@ -26,6 +27,7 @@ const (
GracefulShutdownTimeout
StructuredLogging
DebugDumpRequests
AllowEmailToDomains
)

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

viper.SetDefault(AllowEmailToDomains, []string{})
viper.BindEnv(AllowEmailToDomains, "ALLOW_EMAIL_TO_DOMAINS")

config := Config{
Port: viper.GetInt(Port),
SendgridAPIKey: viper.GetString(SendgridAPIKey),
SlackAPIKey: viper.GetString(SlackAPIKey),
GracefulShutdownTimeout: viper.GetDuration(GracefulShutdownTimeout),
StructuredLogging: viper.GetBool(StructuredLogging),
DebugDumpRequests: viper.GetBool(DebugDumpRequests),
AllowEmailToDomains: viper.GetStringSlice(AllowEmailToDomains),
}

return &config
Expand Down
23 changes: 23 additions & 0 deletions internal/mail/mail.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package mail

import (
"fmt"
"strings"
"sync"

"github.com/commitdev/zero-notification-service/internal/server"
Expand Down Expand Up @@ -81,3 +83,24 @@ func convertAddresses(addresses []server.EmailRecipient) []*sendgridMail.Email {
}
return returnAddresses
}

// RemoveInvalidRecipients accepts a list of recipients and removes the ones with domains not in the allowed list
func RemoveInvalidRecipients(recipients []server.EmailRecipient, allowedDomains []string) []server.EmailRecipient {
valid := []server.EmailRecipient{}
for _, recipient := range recipients {
if addressInAllowedDomain(recipient.Address, allowedDomains) {
valid = append(valid, recipient)
}
}
return valid
}

// addressInAllowedDomain checks if a single email address is in a list of domains
func addressInAllowedDomain(address string, domains []string) bool {
for _, domain := range domains {
if strings.HasSuffix(address, fmt.Sprintf("@%s", domain)) {
return true
}
}
return false
}
44 changes: 32 additions & 12 deletions internal/mail/mail_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,7 @@ func (cl *FakeClient) Send(email *sendgridMail.SGMailV3) (*rest.Response, error)
}

func TestSendBulkMail(t *testing.T) {
var toList []server.EmailRecipient
// Create a random number of mails
rand.Seed(time.Now().UnixNano())
sendCount := rand.Intn(5) + 2
for i := 0; i < sendCount; i++ {
toList = append(toList, server.EmailRecipient{
Name: fmt.Sprintf("Test Recipient %d", i),
Address: fmt.Sprintf("address%d@domain.com", i),
})
}
toList := createRandomRecipients(2, 5)
cc := make([]server.EmailRecipient, 0)
bcc := make([]server.EmailRecipient, 0)
from := server.EmailSender{Name: "Test User", Address: "address@domain.com"}
Expand All @@ -52,8 +43,37 @@ func TestSendBulkMail(t *testing.T) {
returnedCount++
}

assert.Equal(t, sendCount, returnedCount, "Response count should match requests sent")
assert.Equal(t, len(toList), returnedCount, "Response count should match requests sent")

// Check that the send function was called
client.AssertNumberOfCalls(t, "Send", sendCount)
client.AssertNumberOfCalls(t, "Send", len(toList))
}

func TestRemoveInvalidRecipients(t *testing.T) {
toList := createRandomRecipients(2, 5)

originalSize := len(toList)

toList[0].Address = "address@commit.dev"

alteredList := mail.RemoveInvalidRecipients(toList, []string{"commit.dev", "domain.com"})
assert.Equal(t, len(alteredList), originalSize, "All addresses should remain in the list")

alteredList = mail.RemoveInvalidRecipients(toList, []string{"commit.dev"})
assert.Equal(t, len(alteredList), 1, "1 address should remain in the list")
}

// createRandomRecipients creates a random list of recipients
func createRandomRecipients(min int, randCount int) []server.EmailRecipient {
var toList []server.EmailRecipient
// Create a random number of mails
rand.Seed(time.Now().UnixNano())
sendCount := rand.Intn(randCount) + min
for i := 0; i < sendCount; i++ {
toList = append(toList, server.EmailRecipient{
Name: fmt.Sprintf("Test Recipient %d", i),
Address: fmt.Sprintf("address%d@domain.com", i),
})
}
return toList
}
28 changes: 28 additions & 0 deletions internal/service/api_email_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ func NewEmailApiService(c *config.Config) server.EmailApiServicer {

// SendEmail - Send an email
func (s *EmailApiService) SendEmail(ctx context.Context, sendMailRequest server.SendMailRequest) (server.ImplResponse, error) {

// Check if there are valid recipients who are not in the restriction list
if len(s.config.AllowEmailToDomains) > 0 {
originalAddresses := sendMailRequest.ToAddresses
sendMailRequest.ToAddresses = mail.RemoveInvalidRecipients(sendMailRequest.ToAddresses, s.config.AllowEmailToDomains)
// If there are none, return with a 200 but don't send anything
if len(sendMailRequest.ToAddresses) == 0 {
zap.S().Infow("No valid Recipients for send", zap.Any("original_addresses", originalAddresses))
return server.Response(http.StatusOK, server.SendMailResponse{TrackingId: "No valid recipients"}), nil
}
}
sendMailRequest.CcAddresses = mail.RemoveInvalidRecipients(sendMailRequest.CcAddresses, s.config.AllowEmailToDomains)
sendMailRequest.BccAddresses = mail.RemoveInvalidRecipients(sendMailRequest.BccAddresses, s.config.AllowEmailToDomains)

client := sendgrid.NewSendClient(s.config.SendgridAPIKey)
response, err := mail.SendIndividualMail(sendMailRequest.ToAddresses, sendMailRequest.FromAddress, sendMailRequest.CcAddresses, sendMailRequest.BccAddresses, sendMailRequest.Message, client)

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

// 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.
func (s *EmailApiService) SendBulk(ctx context.Context, sendBulkMailRequest server.SendBulkMailRequest) (server.ImplResponse, error) {
// Check if there are valid recipients who are not in the restriction list
if len(s.config.AllowEmailToDomains) > 0 {
originalAddresses := sendBulkMailRequest.ToAddresses
sendBulkMailRequest.ToAddresses = mail.RemoveInvalidRecipients(sendBulkMailRequest.ToAddresses, s.config.AllowEmailToDomains)

// If there are none, return with a 200 but don't send anything
if len(sendBulkMailRequest.ToAddresses) == 0 {
zap.S().Infow("No valid Recipients for bulk send", zap.Any("original_addresses", originalAddresses))
return server.Response(http.StatusOK, server.SendMailResponse{TrackingId: "No valid recipients"}), nil
}
}
sendBulkMailRequest.CcAddresses = mail.RemoveInvalidRecipients(sendBulkMailRequest.CcAddresses, s.config.AllowEmailToDomains)
sendBulkMailRequest.BccAddresses = mail.RemoveInvalidRecipients(sendBulkMailRequest.BccAddresses, s.config.AllowEmailToDomains)

client := sendgrid.NewSendClient(s.config.SendgridAPIKey)

responseChannel := make(chan mail.BulkSendAttempt)
Expand Down