Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for Twilio Email #392

Merged
merged 1 commit into from
May 12, 2020
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
130 changes: 130 additions & 0 deletions base_interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package sendgrid

import (
"errors"
"net/http"
"strconv"
"time"

"github.com/sendgrid/rest"
"github.com/sendgrid/sendgrid-go/helpers/mail"
)

// Version is this client library's current version
const (
Version = "3.5.4"
rateLimitRetry = 5
rateLimitSleep = 1100
)

type options struct {
Auth string
Endpoint string
Host string
Subuser string
}

// Client is the Twilio SendGrid Go client
type Client struct {
rest.Request
}

func (o *options) baseURL() string {
return o.Host + o.Endpoint
}

// requestNew create Request
// @return [Request] a default request object
func requestNew(options options) rest.Request {
requestHeaders := map[string]string{
"Authorization": options.Auth,
"User-Agent": "sendgrid/" + Version + ";go",
"Accept": "application/json",
}

if len(options.Subuser) != 0 {
requestHeaders["On-Behalf-Of"] = options.Subuser
}

return rest.Request{
BaseURL: options.baseURL(),
Headers: requestHeaders,
}
}

// Send sends an email through Twilio SendGrid
func (cl *Client) Send(email *mail.SGMailV3) (*rest.Response, error) {
cl.Body = mail.GetRequestBody(email)
return MakeRequest(cl.Request)
}

// DefaultClient is used if no custom HTTP client is defined
var DefaultClient = rest.DefaultClient

// API sets up the request to the Twilio SendGrid API, this is main interface.
// Please use the MakeRequest or MakeRequestAsync functions instead.
// (deprecated)
func API(request rest.Request) (*rest.Response, error) {
return MakeRequest(request)
}

// MakeRequest attempts a Twilio SendGrid request synchronously.
func MakeRequest(request rest.Request) (*rest.Response, error) {
return DefaultClient.Send(request)
}

// MakeRequestRetry a synchronous request, but retry in the event of a rate
// limited response.
func MakeRequestRetry(request rest.Request) (*rest.Response, error) {
retry := 0
var response *rest.Response
var err error

for {
response, err = MakeRequest(request)
if err != nil {
return nil, err
}

if response.StatusCode != http.StatusTooManyRequests {
return response, nil
}

if retry > rateLimitRetry {
return nil, errors.New("rate limit retry exceeded")
}
retry++

resetTime := time.Now().Add(rateLimitSleep * time.Millisecond)

reset, ok := response.Headers["X-RateLimit-Reset"]
if ok && len(reset) > 0 {
t, err := strconv.Atoi(reset[0])
if err == nil {
resetTime = time.Unix(int64(t), 0)
}
}
time.Sleep(resetTime.Sub(time.Now()))
}
}

// MakeRequestAsync attempts a request asynchronously in a new go
// routine. This function returns two channels: responses
// and errors. This function will retry in the case of a
// rate limit.
func MakeRequestAsync(request rest.Request) (chan *rest.Response, chan error) {
r := make(chan *rest.Response)
e := make(chan error)

go func() {
response, err := MakeRequestRetry(request)
if err != nil {
e <- err
}
if response != nil {
r <- response
}
}()

return r, e
}
137 changes: 15 additions & 122 deletions sendgrid.go
Original file line number Diff line number Diff line change
@@ -1,80 +1,44 @@
// Package sendgrid provides a simple interface to interact with the Twilio SendGrid API
package sendgrid

import (
"errors"
"net/http"
"strconv"
"time"

"github.com/sendgrid/rest" // depends on version 2.2.0
"github.com/sendgrid/sendgrid-go/helpers/mail"
)

// Version is this client library's current version
const (
Version = "3.5.4"
rateLimitRetry = 5
rateLimitSleep = 1100
"github.com/sendgrid/rest"
)

// Client is the Twilio SendGrid Go client
type Client struct {
// rest.Request
rest.Request
}

// options for requestNew
type options struct {
// sendGridOptions for CreateRequest
type sendGridOptions struct {
Key string
Endpoint string
Host string
Subuser string
}

func (o *options) baseURL() string {
return o.Host + o.Endpoint
}

// GetRequest
// @return [Request] a default request object
func GetRequest(key, endpoint, host string) rest.Request {
return requestNew(options{key, endpoint, host, ""})
return createSendGridRequest(sendGridOptions{key, endpoint, host, ""})
}

// GetRequestSubuser like GetRequest but with On-Behalf of Subuser
// @return [Request] a default request object
func GetRequestSubuser(key, endpoint, host, subuser string) rest.Request {
return requestNew(options{key, endpoint, host, subuser})
return createSendGridRequest(sendGridOptions{key, endpoint, host, subuser})
}

// requestNew create Request
// createSendGridRequest create Request
// @return [Request] a default request object
func requestNew(options options) rest.Request {
if options.Host == "" {
options.Host = "https://api.sendgrid.com"
func createSendGridRequest(sgOptions sendGridOptions) rest.Request {
options := options{
"Bearer " + sgOptions.Key,
sgOptions.Endpoint,
sgOptions.Host,
sgOptions.Subuser,
}

requestHeaders := map[string]string{
"Authorization": "Bearer " + options.Key,
"User-Agent": "sendgrid/" + Version + ";go",
"Accept": "application/json",
}

if len(options.Subuser) != 0 {
requestHeaders["On-Behalf-Of"] = options.Subuser
}

return rest.Request{
BaseURL: options.baseURL(),
Headers: requestHeaders,
if options.Host == "" {
options.Host = "https://api.sendgrid.com"
}
}

// Send sends an email through Twilio SendGrid
func (cl *Client) Send(email *mail.SGMailV3) (*rest.Response, error) {
cl.Body = mail.GetRequestBody(email)
return MakeRequest(cl.Request)
return requestNew(options)
}

// NewSendClient constructs a new Twilio SendGrid client given an API key
Expand All @@ -91,74 +55,3 @@ func NewSendClientSubuser(key, subuser string) *Client {
request.Method = "POST"
return &Client{request}
}

// DefaultClient is used if no custom HTTP client is defined
var DefaultClient = rest.DefaultClient

// API sets up the request to the Twilio SendGrid API, this is main interface.
// Please use the MakeRequest or MakeRequestAsync functions instead.
// (deprecated)
func API(request rest.Request) (*rest.Response, error) {
return MakeRequest(request)
}

// MakeRequest attempts a Twilio SendGrid request synchronously.
func MakeRequest(request rest.Request) (*rest.Response, error) {
return DefaultClient.Send(request)
}

// MakeRequestRetry a synchronous request, but retry in the event of a rate
// limited response.
func MakeRequestRetry(request rest.Request) (*rest.Response, error) {
retry := 0
var response *rest.Response
var err error

for {
response, err = MakeRequest(request)
if err != nil {
return nil, err
}

if response.StatusCode != http.StatusTooManyRequests {
return response, nil
}

if retry > rateLimitRetry {
return nil, errors.New("Rate limit retry exceeded")
}
retry++

resetTime := time.Now().Add(rateLimitSleep * time.Millisecond)

reset, ok := response.Headers["X-RateLimit-Reset"]
if ok && len(reset) > 0 {
t, err := strconv.Atoi(reset[0])
if err == nil {
resetTime = time.Unix(int64(t), 0)
}
}
time.Sleep(resetTime.Sub(time.Now()))
}
}

// MakeRequestAsync attempts a request asynchronously in a new go
// routine. This function returns two channels: responses
// and errors. This function will retry in the case of a
// rate limit.
func MakeRequestAsync(request rest.Request) (chan *rest.Response, chan error) {
r := make(chan *rest.Response)
e := make(chan error)

go func() {
response, err := MakeRequestRetry(request)
if err != nil {
e <- err
}
if response != nil {
r <- response
}
}()

return r, e
}
6 changes: 3 additions & 3 deletions sendgrid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func TestRequestRetry_rateLimit(t *testing.T) {
DefaultClient = &custom
_, err := MakeRequestRetry(request)
assert.NotNil(t, err, "An error did not trigger")
assert.True(t, strings.Contains(err.Error(), "Rate limit retry exceeded"), "We did not receive the rate limit error")
assert.True(t, strings.Contains(err.Error(), "rate limit retry exceeded"), "We did not receive the rate limit error")
DefaultClient = rest.DefaultClient
}

Expand All @@ -146,7 +146,7 @@ func TestRequestRetry_rateLimit_noHeader(t *testing.T) {
DefaultClient = &custom
_, err := MakeRequestRetry(request)
assert.NotNil(t, err, "An error did not trigger")
assert.True(t, strings.Contains(err.Error(), "Rate limit retry exceeded"), "We did not receive the rate limit error")
assert.True(t, strings.Contains(err.Error(), "rate limit retry exceeded"), "We did not receive the rate limit error")
DefaultClient = rest.DefaultClient
}

Expand Down Expand Up @@ -194,7 +194,7 @@ func TestRequestAsync_rateLimit(t *testing.T) {
t.Error("Received a valid response")
return
case err := <-e:
assert.True(t, strings.Contains(err.Error(), "Rate limit retry exceeded"), "We did not receive the rate limit error")
assert.True(t, strings.Contains(err.Error(), "rate limit retry exceeded"), "We did not receive the rate limit error")
case <-time.After(10 * time.Second):
t.Error("Timed out waiting for an error")
}
Expand Down
41 changes: 41 additions & 0 deletions twilio_email.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package sendgrid

import (
"encoding/base64"

"github.com/sendgrid/rest"
)

// TwilioEmailOptions for GetTwilioEmailRequest
type TwilioEmailOptions struct {
Username string
Password string
Endpoint string
Host string
}

// 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}
}

// GetTwilioEmailRequest create Request
// @return [Request] a default request object
func GetTwilioEmailRequest(twilioEmailOptions TwilioEmailOptions) rest.Request {
credentials := twilioEmailOptions.Username + ":" + twilioEmailOptions.Password
encodedCreds := base64.StdEncoding.EncodeToString([]byte(credentials))

options := options{
Auth: "Basic " + encodedCreds,
Endpoint: twilioEmailOptions.Endpoint,
Host: twilioEmailOptions.Host,
}

if options.Host == "" {
options.Host = "https://email.twilio.com"
}

return requestNew(options)
}
Loading