Skip to content

Commit

Permalink
add 'message.replyToAddresses' config option
Browse files Browse the repository at this point in the history
  • Loading branch information
ashutoshgngwr committed Aug 3, 2023
1 parent 69dcbbb commit d61841b
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 61 deletions.
1 change: 1 addition & 0 deletions internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var defaultConfig = &config.Config{
},
Message: config.MessageConfig{
Sender: "Iris CLI <iris@example.test>",
ReplyToAddresses: []string{"inbox@example.test", "another@example.test"},
DefaultDataCsvFile: "default.csv",
RecipientDataCsvFile: "recipients.csv",
RecipientEmailColumnName: "Email",
Expand Down
12 changes: 7 additions & 5 deletions internal/cmd/send.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,17 @@ func SendCommand(v *viper.Viper) *cobra.Command {
return err
}

e, err := t.Render(recipientData)
msg, err := t.Render(recipientData)
if err != nil {
return err
}

sender := cfg.Message.Sender
recipient := recipientData[cfg.Message.RecipientEmailColumnName]
cmd.Println("dispatching to", recipient)
if err := svc.Send(sender, recipient, e); err != nil {
if err := svc.Send(&email.SendOptions{
From: cfg.Message.Sender,
To: recipientData[cfg.Message.RecipientEmailColumnName],
ReplyTo: cfg.Message.ReplyToAddresses,
Message: msg,
}); err != nil {
return err
}
}
Expand Down
11 changes: 6 additions & 5 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ type AwsSesServiceConfig struct {
}

type MessageConfig struct {
Sender string `yaml:"sender,omitempty"`
DefaultDataCsvFile string `yaml:"defaultDataCsvFile,omitempty"`
RecipientDataCsvFile string `yaml:"recipientDataCsvFile,omitempty"`
RecipientEmailColumnName string `yaml:"recipientEmailColumnName,omitempty"`
MinifyHtml bool `yaml:"minifyHtml,omitempty"`
Sender string `yaml:"sender,omitempty"`
ReplyToAddresses []string `yaml:"replyToAddresses,omitempty"`
DefaultDataCsvFile string `yaml:"defaultDataCsvFile,omitempty"`
RecipientDataCsvFile string `yaml:"recipientDataCsvFile,omitempty"`
RecipientEmailColumnName string `yaml:"recipientEmailColumnName,omitempty"`
MinifyHtml bool `yaml:"minifyHtml,omitempty"`
}

// Read attempts to read the config file in the current working directory. It
Expand Down
53 changes: 35 additions & 18 deletions internal/email/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ import (
)

type Service interface {
Send(from string, to string, m *Message) error
Send(opts *SendOptions) error
}

type SendOptions struct {
From string
To string
ReplyTo []string
Message *Message
}

type ServiceOption func(upstream Service) Service
Expand Down Expand Up @@ -58,32 +65,36 @@ type awsSesService struct {
client AwsSesClient
}

func (s *awsSesService) Send(from string, to string, m *Message) error {
if m == nil {
func (s *awsSesService) Send(opts *SendOptions) error {
if opts == nil {
return fmt.Errorf("send options must not be nil")
}

if opts.Message == nil {
return fmt.Errorf("message must not be nil")
}

if _, err := s.client.SendEmail(&ses.SendEmailInput{
Source: aws.String(from),
Source: aws.String(opts.From),
Destination: &ses.Destination{
CcAddresses: []*string{},
ToAddresses: []*string{
aws.String(to),
aws.String(opts.To),
},
},
ReplyToAddresses: aws.StringSlice(opts.ReplyTo),
Message: &ses.Message{
Subject: &ses.Content{
Charset: aws.String("utf-8"),
Data: aws.String(m.Subject),
Data: aws.String(opts.Message.Subject),
},
Body: &ses.Body{
Text: &ses.Content{
Charset: aws.String("utf-8"),
Data: aws.String(m.TextBody),
Data: aws.String(opts.Message.TextBody),
},
Html: &ses.Content{
Charset: aws.String("utf-8"),
Data: aws.String(m.HtmlBody),
Data: aws.String(opts.Message.HtmlBody),
},
},
},
Expand All @@ -102,8 +113,12 @@ type printService struct {
w io.Writer
}

func (s *printService) Send(from string, to string, m *Message) error {
if m == nil {
func (s *printService) Send(opts *SendOptions) error {
if opts == nil {
return fmt.Errorf("send options must not be nil")
}

if opts.Message == nil {
return fmt.Errorf("message must not be nil")
}

Expand All @@ -121,9 +136,9 @@ func (s *printService) Send(from string, to string, m *Message) error {
tw.SetAutoWrapText(false)
tw.SetRowLine(true)
tw.AppendBulk([][]string{
{"Subject", wordwrap.WrapString(m.Subject, uint(pw))},
{"Text Body", wordwrap.WrapString(m.TextBody, uint(pw))},
{"HTML Body", wordwrap.WrapString(m.HtmlBody, uint(pw))},
{"Subject", wordwrap.WrapString(opts.Message.Subject, uint(pw))},
{"Text Body", wordwrap.WrapString(opts.Message.TextBody, uint(pw))},
{"HTML Body", wordwrap.WrapString(opts.Message.HtmlBody, uint(pw))},
})
tw.Render()
return nil
Expand Down Expand Up @@ -151,9 +166,9 @@ type rateLimitedService struct {
limiter ratelimit.Limiter
}

func (s *rateLimitedService) Send(from string, to string, m *Message) error {
func (s *rateLimitedService) Send(opts *SendOptions) error {
s.limiter.Take()
return s.upstream.Send(from, to, m)
return s.upstream.Send(opts)
}

func WithRetries(retryCount int) ServiceOption {
Expand All @@ -170,10 +185,10 @@ type retryService struct {
retryCount int
}

func (s *retryService) Send(from string, to string, m *Message) error {
func (s *retryService) Send(opts *SendOptions) error {
var err error
for i := 0; i <= s.retryCount; i++ {
err = s.upstream.Send(from, to, m)
err = s.upstream.Send(opts)
if err == nil {
break
}
Expand All @@ -182,6 +197,8 @@ func (s *retryService) Send(from string, to string, m *Message) error {
return err
}

// ApplyOptions wraps the given `upstream` service in the given service options
// (decorators).
func ApplyOptions(upstream Service, opts ...ServiceOption) Service {
s := upstream
for _, option := range opts {
Expand Down
85 changes: 52 additions & 33 deletions internal/email/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ses"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -16,39 +17,48 @@ func TestAwsSesService(t *testing.T) {
t.Run("WithNilMessage", func(t *testing.T) {
c := &FakeAwsSesClient{RespondWithOutput: &ses.SendEmailOutput{}}
s := email.NewAwsSesServiceWithClient(c)
err := s.Send("test-from", "test-to", nil)
err := s.Send(&email.SendOptions{
From: "test-from",
To: "test-to",
})
assert.Error(t, err)
})

t.Run("WithUpstreamError", func(t *testing.T) {
c := &FakeAwsSesClient{RespondWithError: fmt.Errorf("test-error")}
s := email.NewAwsSesServiceWithClient(c)
err := s.Send("test-from", "test-to", &email.Message{})
err := s.Send(&email.SendOptions{
From: "test-from",
To: "test-to",
Message: &email.Message{},
})
assert.Error(t, err)
})

t.Run("WithNoError", func(t *testing.T) {
const from = "test-from"
const to = "test-to"
const subject = "test-subject"
const textBody = "test-text-body"
const htmlBody = "test-html-body"
sendOpts := &email.SendOptions{
From: "test-from",
To: "test-to",
ReplyTo: []string{"test-reply-to"},
Message: &email.Message{
Subject: "test-subject",
TextBody: "test-text-body",
HtmlBody: "test-html-body",
},
}

c := &FakeAwsSesClient{RespondWithOutput: &ses.SendEmailOutput{}}
s := email.NewAwsSesServiceWithClient(c)
err := s.Send(from, to, &email.Message{
Subject: subject,
TextBody: textBody,
HtmlBody: htmlBody,
})

err := s.Send(sendOpts)
assert.NoError(t, err)

i := c.LastSendEmailInput
assert.Equal(t, from, *i.Source)
assert.Equal(t, to, *i.Destination.ToAddresses[0])
assert.Equal(t, subject, *i.Message.Subject.Data)
assert.Equal(t, textBody, *i.Message.Body.Text.Data)
assert.Equal(t, htmlBody, *i.Message.Body.Html.Data)
assert.Equal(t, sendOpts.From, *i.Source)
assert.Equal(t, sendOpts.To, *i.Destination.ToAddresses[0])
assert.Equal(t, sendOpts.ReplyTo, aws.StringValueSlice(i.ReplyToAddresses))
assert.Equal(t, sendOpts.Message.Subject, *i.Message.Subject.Data)
assert.Equal(t, sendOpts.Message.TextBody, *i.Message.Body.Text.Data)
assert.Equal(t, sendOpts.Message.HtmlBody, *i.Message.Body.Html.Data)
})
}

Expand All @@ -65,32 +75,37 @@ func (c *FakeAwsSesClient) SendEmail(input *ses.SendEmailInput) (*ses.SendEmailO
}

func TestPrintService(t *testing.T) {
const subject = "test-subject"
const textBody = "test-text-body"
const htmlBody = "test-html-body"
sendOpts := &email.SendOptions{
From: "test-from",
To: "test-to",
Message: &email.Message{
Subject: "test-subject",
TextBody: "test-text-body",
HtmlBody: "test-html-body",
},
}

b := &bytes.Buffer{}
s := email.NewPrintService(b)
err := s.Send("test-from", "test-to", &email.Message{
Subject: subject,
TextBody: textBody,
HtmlBody: htmlBody,
})

err := s.Send(sendOpts)
assert.NoError(t, err)

out := b.String()
assert.Contains(t, out, subject)
assert.Contains(t, out, textBody)
assert.Contains(t, out, htmlBody)
assert.Contains(t, out, sendOpts.Message.Subject)
assert.Contains(t, out, sendOpts.Message.TextBody)
assert.Contains(t, out, sendOpts.Message.HtmlBody)
}

func TestRateLimitedService(t *testing.T) {
var s email.Service = &unreliableService{errorsBeforeSucceeding: 0}
s = email.ApplyOptions(s, email.WithRateLimit(1))
then := time.Now()
for i := 0; i < 5; i++ {
err := s.Send("test", "test", &email.Message{})
err := s.Send(&email.SendOptions{
From: "test-from",
To: "test-to",
Message: &email.Message{},
})
require.NoError(t, err)
}

Expand Down Expand Up @@ -135,7 +150,11 @@ func TestRetryService(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
var s email.Service = &unreliableService{errorsBeforeSucceeding: test.errorCount}
s = email.ApplyOptions(s, email.WithRetries(test.retryCount))
err := s.Send("test", "test", &email.Message{})
err := s.Send(&email.SendOptions{
From: "test-from",
To: "test-to",
Message: &email.Message{},
})
if test.wantErr {
assert.Error(t, err)
} else {
Expand All @@ -149,7 +168,7 @@ type unreliableService struct {
errorsBeforeSucceeding int
}

func (s *unreliableService) Send(from string, to string, m *email.Message) error {
func (s *unreliableService) Send(opts *email.SendOptions) error {
s.errorsBeforeSucceeding--
if s.errorsBeforeSucceeding > -1 {
return fmt.Errorf("test-error")
Expand Down

0 comments on commit d61841b

Please sign in to comment.