diff --git a/base_interface.go b/base_interface.go index 6fdaf3cf..8d64756f 100644 --- a/base_interface.go +++ b/base_interface.go @@ -29,7 +29,8 @@ type options struct { // Client is the Twilio SendGrid Go client type Client struct { - rest.Request + apiKey string + emailOptions TwilioEmailOptions } func (o *options) baseURL() string { @@ -57,19 +58,41 @@ func requestNew(options options) rest.Request { // Send sends an email through Twilio SendGrid func (cl *Client) Send(email *mail.SGMailV3) (*rest.Response, error) { - return cl.SendWithContext(context.Background(), email) + return cl.SendWithContext(context.Background(), email, nil) } -// SendWithContext sends an email through Twilio SendGrid with context.Context. -func (cl *Client) SendWithContext(ctx context.Context, email *mail.SGMailV3) (*rest.Response, error) { - cl.Body = mail.GetRequestBody(email) +// SendWithHeaders sends an email through Twilio SendGrid with additional headers +func (cl *Client) SendWithHeaders(email *mail.SGMailV3, headers map[string]string) (*rest.Response, error) { + return cl.SendWithContext(context.Background(), email, headers) +} + +// SendWithContext sends an email through Twilio SendGrid with context.Context +func (cl *Client) SendWithContext(ctx context.Context, email *mail.SGMailV3, headers map[string]string) (*rest.Response, error) { + var request rest.Request + + if cl.apiKey != "" { + request = GetRequest(cl.apiKey, "/v3/mail/send", "") + } else if cl.emailOptions != (TwilioEmailOptions{}) { + request = GetTwilioEmailRequest(cl.emailOptions) + } else { + return nil, errors.New("no API key or email options provided") + } + + request.Method = "POST" + + // Add any custom headers provided by the caller. + for k, v := range headers { + request.Headers[k] = v + } + + request.Body = mail.GetRequestBody(email) // when Content-Encoding header is set to "gzip" // mail body is compressed using gzip according to // https://docs.sendgrid.com/api-reference/mail-send/mail-send#mail-body-compression - if cl.Headers["Content-Encoding"] == "gzip" { + if request.Headers["Content-Encoding"] == "gzip" { var gzipped bytes.Buffer gz := gzip.NewWriter(&gzipped) - if _, err := gz.Write(cl.Body); err != nil { + if _, err := gz.Write(request.Body); err != nil { return nil, err } if err := gz.Flush(); err != nil { @@ -79,9 +102,9 @@ func (cl *Client) SendWithContext(ctx context.Context, email *mail.SGMailV3) (*r return nil, err } - cl.Body = gzipped.Bytes() + request.Body = gzipped.Bytes() } - return MakeRequestWithContext(ctx, cl.Request) + return MakeRequestWithContext(ctx, request) } // DefaultClient is used if no custom HTTP client is defined diff --git a/go.coverage.sh b/go.coverage.sh index fadb3cc8..07442729 100755 --- a/go.coverage.sh +++ b/go.coverage.sh @@ -4,7 +4,7 @@ set -e echo > coverage.txt for d in $(go list ./... | grep -v -E '/vendor|/examples|/docker'); do - go test -coverprofile=profile.out -covermode=atomic "$d" + go test -race -coverprofile=profile.out -covermode=atomic "$d" if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out diff --git a/sendgrid.go b/sendgrid.go index 1c50451c..01d242d2 100644 --- a/sendgrid.go +++ b/sendgrid.go @@ -2,8 +2,9 @@ package sendgrid import ( "errors" - "github.com/sendgrid/rest" "net/url" + + "github.com/sendgrid/rest" ) // sendGridOptions for CreateRequest @@ -51,9 +52,7 @@ func createSendGridRequest(sgOptions sendGridOptions) rest.Request { // NewSendClient constructs a new Twilio SendGrid client given an API key func NewSendClient(key string) *Client { - request := GetRequest(key, "/v3/mail/send", "") - request.Method = "POST" - return &Client{request} + return &Client{apiKey: key} } // extractEndpoint extracts the endpoint from a baseURL diff --git a/sendgrid_test.go b/sendgrid_test.go index 57b52709..086eb1e3 100644 --- a/sendgrid_test.go +++ b/sendgrid_test.go @@ -9,6 +9,7 @@ import ( "os" "strconv" "strings" + "sync" "testing" "time" @@ -1640,10 +1641,52 @@ func Test_test_mail_batch__batch_id__get(t *testing.T) { assert.Equal(t, 200, response.StatusCode, "Wrong status code returned") } +func Test_test_client_send_is_thread_safe(t *testing.T) { + apiKey := "SENDGIRD_APIKEY" + const numRequests = 5 + var wg sync.WaitGroup + client := NewSendClient(apiKey) + + // Launch multiple goroutines to send concurrent requests + for i := 0; i < numRequests; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + from := &mail.Email{ + Name: "Name", + Address: "sam.smith@example.com", + } + + to := &mail.Email{ + Name: "Recipient", + Address: "jane.doe@example.com", + } + + email := &mail.SGMailV3{ + From: from, + Personalizations: []*mail.Personalization{ + { + To: []*mail.Email{to}, + Subject: "Subject", + }, + }, + Content: []*mail.Content{ + { + Type: "text/plain", + Value: "Value", + }, + }, + } + + client.Send(email) + }(i) + } + wg.Wait() +} + func Test_test_send_client_with_mail_body_compression_enabled(t *testing.T) { apiKey := "SENDGRID_API_KEY" client := NewSendClient(apiKey) - client.Headers["Content-Encoding"] = "gzip" emailBytes := []byte(` { "asm": { @@ -1780,8 +1823,10 @@ func Test_test_send_client_with_mail_body_compression_enabled(t *testing.T) { email := &mail.SGMailV3{} err := json.Unmarshal(emailBytes, email) assert.Nil(t, err, fmt.Sprintf("Unmarshal error: %v", err)) - client.Request.Headers["X-Mock"] = "202" - response, err := client.Send(email) + + headers := map[string]string{"Content-Encoding": "gzip", "X-Mock": "202"} + + response, err := client.SendWithHeaders(email, headers) if err != nil { t.Log(err) } @@ -1929,8 +1974,7 @@ func Test_test_send_client(t *testing.T) { email := &mail.SGMailV3{} err := json.Unmarshal(emailBytes, email) assert.Nil(t, err, fmt.Sprintf("Unmarshal error: %v", err)) - client.Request.Headers["X-Mock"] = "202" - response, err := client.Send(email) + response, err := client.SendWithHeaders(email, map[string]string{"X-Mock": "202"}) if err != nil { t.Log(err) } diff --git a/twilio_email.go b/twilio_email.go index 52981a41..1d39c848 100644 --- a/twilio_email.go +++ b/twilio_email.go @@ -16,9 +16,8 @@ type TwilioEmailOptions struct { // NewTwilioEmailSendClient constructs a new Twilio Email client given a username and password func NewTwilioEmailSendClient(username, password string) *Client { - request := GetTwilioEmailRequest(TwilioEmailOptions{Username: username, Password: password, Endpoint: "/v3/mail/send"}) - request.Method = "POST" - return &Client{request} + emailOptions := &TwilioEmailOptions{Username: username, Password: password, Endpoint: "/v3/mail/send"} + return &Client{emailOptions: *emailOptions} } // GetTwilioEmailRequest create Request diff --git a/twilio_email_test.go b/twilio_email_test.go index 51f392c6..16d14cf7 100644 --- a/twilio_email_test.go +++ b/twilio_email_test.go @@ -13,8 +13,9 @@ import ( func TestNewTwilioEmailSendClient(t *testing.T) { mailClient := NewTwilioEmailSendClient("username", "password") - assert.Equal(t, "https://email.twilio.com/v3/mail/send", mailClient.BaseURL) - assert.Equal(t, "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", mailClient.Headers["Authorization"]) + request := GetTwilioEmailRequest(mailClient.emailOptions) + assert.Equal(t, "https://email.twilio.com/v3/mail/send", request.BaseURL) + assert.Equal(t, "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", request.Headers["Authorization"]) } func TestGetTwilioEmailRequest(t *testing.T) {