From f4ff6d09d029626ee8e8921a21f7d1a281069de6 Mon Sep 17 00:00:00 2001 From: "Derrick J. Wippler" Date: Fri, 11 Jan 2019 13:36:10 -0600 Subject: [PATCH] ListBounces() now returns a page iterator * API responses with CreatedAt fields are now unmarshalled into RFC2822 * Removed GetCode() from `Bounce` struct. Verified API returns 'string' and not 'int' --- acceptance_test.go | 21 ------ bounces.go | 142 ++++++++++++++++++++++++++++++---------- bounces_test.go | 83 +++++++++++------------ credentials.go | 6 +- credentials_test.go | 3 +- domains.go | 17 ++--- mailgun.go | 4 +- mailgun_test.go | 37 ----------- mailing_lists.go | 12 ++-- members.go | 2 +- messages_test.go | 19 +++--- mock_domains.go | 5 +- mock_mailing_list.go | 5 +- mock_routes.go | 3 +- rfc2822.go | 52 +++++++++++++++ routes.go | 2 +- spam_complaints.go | 6 +- spam_complaints_test.go | 1 - stats.go | 17 +++-- stats_test.go | 2 +- unsubscribes.go | 26 ++------ unsubscribes_test.go | 17 ++--- 22 files changed, 252 insertions(+), 230 deletions(-) create mode 100644 rfc2822.go diff --git a/acceptance_test.go b/acceptance_test.go index 19b6d77d..c6154edf 100644 --- a/acceptance_test.go +++ b/acceptance_test.go @@ -2,7 +2,6 @@ package mailgun import ( "bytes" - "crypto/rand" "fmt" "io" "io/ioutil" @@ -12,8 +11,6 @@ import ( "net/url" "os" "testing" - - "github.com/facebookgo/ensure" ) // Return the variable missing which caused the test to be skipped @@ -26,24 +23,6 @@ func SkipNetworkTest() string { return "" } -// Many tests require configuration settings unique to the user, passed in via -// environment variables. If these variables aren't set, we need to fail the test early. -func reqEnv(t *testing.T, variableName string) string { - value := os.Getenv(variableName) - ensure.True(t, value != "") - return value -} - -func randomDomainURL(n int) string { - const alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - var bytes = make([]byte, n) - rand.Read(bytes) - for i, b := range bytes { - bytes[i] = alpha[b%byte(len(alpha))] - } - return "http://" + string(bytes) + ".com" -} - func spendMoney(t *testing.T, tFunc func()) { ok := os.Getenv("MG_SPEND_MONEY") if ok != "" { diff --git a/bounces.go b/bounces.go index ec7891b5..70f1cda1 100644 --- a/bounces.go +++ b/bounces.go @@ -3,7 +3,6 @@ package mailgun import ( "context" "strconv" - "time" ) // Bounce aggregates data relating to undeliverable messages to a specific intended recipient, @@ -12,8 +11,8 @@ import ( // while Error provides a human readable reason why. // CreatedAt provides the time at which Mailgun detected the bounce. type Bounce struct { - CreatedAt string `json:"created_at"` - Code interface{} `json:"code"` + CreatedAt RFC2822Time `json:"created_at"` + Code string `json:"code"` Address string `json:"address"` Error string `json:"error"` } @@ -25,56 +24,127 @@ type Paging struct { Last string `json:"last,omitempty"` } -type bounceEnvelope struct { +type bouncesListResponse struct { Items []Bounce `json:"items"` Paging Paging `json:"paging"` } -// GetCreatedAt parses the textual, RFC-822 timestamp into a standard Go-compatible -// Time structure. -func (i Bounce) GetCreatedAt() (t time.Time, err error) { - return parseMailgunTime(i.CreatedAt) -} - -// GetCode will return the bounce code for the message, regardless if it was -// returned as a string or as an integer. This method overcomes a protocol -// bug in the Mailgun API. -func (b Bounce) GetCode() (int, error) { - switch c := b.Code.(type) { - case int: - return c, nil - case string: - return strconv.Atoi(c) - default: - return -1, strconv.ErrSyntax - } -} - // ListBounces returns a complete set of bounces logged against the sender's domain, if any. // The results include the total number of bounces (regardless of skip or limit settings), // and the slice of bounces specified, if successful. // Note that the length of the slice may be smaller than the total number of bounces. -func (mg *MailgunImpl) ListBounces(ctx context.Context, opts *ListOptions) ([]Bounce, error) { +func (mg *MailgunImpl) ListBounces(opts *ListOptions) *BouncesIterator { r := newHTTPRequest(generateApiUrl(mg, bouncesEndpoint)) + r.setClient(mg.Client()) + r.setBasicAuth(basicAuthUser, mg.APIKey()) + if opts != nil { + if opts.Limit != 0 { + r.addParameter("limit", strconv.Itoa(opts.Limit)) + } + } + url, err := r.generateUrlWithParameters() + return &BouncesIterator{ + mg: mg, + bouncesListResponse: bouncesListResponse{Paging: Paging{Next: url, First: url}}, + err: err, + } +} + +type BouncesIterator struct { + bouncesListResponse + mg Mailgun + err error +} - if opts != nil && opts.Limit != 0 { - r.addParameter("limit", strconv.Itoa(opts.Limit)) +// If an error occurred during iteration `Err()` will return non nil +func (ci *BouncesIterator) Err() error { + return ci.err +} + +// Retrieves the next page of items from the api. Returns false when there +// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve +// the error +func (ci *BouncesIterator) Next(ctx context.Context, items *[]Bounce) bool { + if ci.err != nil { + return false + } + ci.err = ci.fetch(ctx, ci.Paging.Next) + if ci.err != nil { + return false } + cpy := make([]Bounce, len(ci.Items)) + copy(cpy, ci.Items) + *items = cpy + if len(ci.Items) == 0 { + return false + } + return true +} - if opts != nil && opts.Skip != 0 { - r.addParameter("skip", strconv.Itoa(opts.Skip)) +// Retrieves the first page of items from the api. Returns false if there +// was an error. It also sets the iterator object to the first page. +// Use `.Err()` to retrieve the error. +func (ci *BouncesIterator) First(ctx context.Context, items *[]Bounce) bool { + if ci.err != nil { + return false } + ci.err = ci.fetch(ctx, ci.Paging.First) + if ci.err != nil { + return false + } + cpy := make([]Bounce, len(ci.Items)) + copy(cpy, ci.Items) + *items = cpy + return true +} - r.setClient(mg.Client()) - r.setBasicAuth(basicAuthUser, mg.APIKey()) +// Retrieves the last page of items from the api. +// Calling Last() is invalid unless you first call First() or Next() +// Returns false if there was an error. It also sets the iterator object +// to the last page. Use `.Err()` to retrieve the error. +func (ci *BouncesIterator) Last(ctx context.Context, items *[]Bounce) bool { + if ci.err != nil { + return false + } + ci.err = ci.fetch(ctx, ci.Paging.Last) + if ci.err != nil { + return false + } + cpy := make([]Bounce, len(ci.Items)) + copy(cpy, ci.Items) + *items = cpy + return true +} - var response bounceEnvelope - err := getResponseFromJSON(ctx, r, &response) - if err != nil { - return nil, err +// Retrieves the previous page of items from the api. Returns false when there +// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve +// the error if any +func (ci *BouncesIterator) Previous(ctx context.Context, items *[]Bounce) bool { + if ci.err != nil { + return false + } + if ci.Paging.Previous == "" { + return false + } + ci.err = ci.fetch(ctx, ci.Paging.Previous) + if ci.err != nil { + return false } + cpy := make([]Bounce, len(ci.Items)) + copy(cpy, ci.Items) + *items = cpy + if len(ci.Items) == 0 { + return false + } + return true +} + +func (ci *BouncesIterator) fetch(ctx context.Context, url string) error { + r := newHTTPRequest(url) + r.setClient(ci.mg.Client()) + r.setBasicAuth(basicAuthUser, ci.mg.APIKey()) - return response.Items, nil + return getResponseFromJSON(ctx, r, &ci.bouncesListResponse) } // GetBounce retrieves a single bounce record, if any exist, for the given recipient address. diff --git a/bounces_test.go b/bounces_test.go index f355086b..3b5001d1 100644 --- a/bounces_test.go +++ b/bounces_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "os" "strings" "testing" @@ -19,8 +20,15 @@ func TestGetBounces(t *testing.T) { ensure.Nil(t, err) ctx := context.Background() - _, err = mg.ListBounces(ctx, nil) - ensure.Nil(t, err) + it := mg.ListBounces(nil) + + var page []Bounce + for it.Next(ctx, &page) { + for _, bounce := range page { + t.Logf("Bounce: %+v\n", bounce) + } + } + ensure.Nil(t, it.Err()) } func TestGetSingleBounce(t *testing.T) { @@ -28,12 +36,12 @@ func TestGetSingleBounce(t *testing.T) { t.Skip(reason) } - domain := reqEnv(t, "MG_DOMAIN") mg, err := NewMailgunFromEnv() ensure.Nil(t, err) ctx := context.Background() - exampleEmail := fmt.Sprintf("%s@%s", strings.ToLower(randomString(64, "")), domain) + exampleEmail := fmt.Sprintf("%s@%s", strings.ToLower(randomString(64, "")), + os.Getenv("MG_DOMAIN")) _, err = mg.GetBounce(ctx, exampleEmail) ensure.NotNil(t, err) @@ -47,70 +55,55 @@ func TestAddDelBounces(t *testing.T) { t.Skip(reason) } - domain := reqEnv(t, "MG_DOMAIN") + domain := os.Getenv("MG_DOMAIN") mg, err := NewMailgunFromEnv() + ctx := context.Background() ensure.Nil(t, err) - // Compute an e-mail address for our domain. - exampleEmail := fmt.Sprintf("%s@%s", strings.ToLower(randomString(8, "bounce")), domain) + findBounce := func(address string) bool { + it := mg.ListBounces(nil) + var page []Bounce + for it.Next(ctx, &page) { + ensure.True(t, len(page) != 0) + for _, bounce := range page { + t.Logf("Bounce Address: %s\n", bounce.Address) + if bounce.Address == address { + return true + } + } + } + if it.Err() != nil { + t.Logf("BounceIterator err: %s", it.Err()) + } + return false + } - // First, basic sanity check. - // Fail early if we have bounces for a fictitious e-mail address. + // Compute an e-mail address for our Bounce. + exampleEmail := fmt.Sprintf("%s@%s", strings.ToLower(randomString(8, "bounce")), domain) - ctx := context.Background() - _, err = mg.ListBounces(ctx, nil) - ensure.Nil(t, err) // Add the bounce for our address. - err = mg.AddBounce(ctx, exampleEmail, "550", "TestAddDelBounces-generated error") ensure.Nil(t, err) // We should now have one bounce listed when we query the API. - - bounces, err := mg.ListBounces(ctx,nil) - ensure.Nil(t, err) - if len(bounces) == 0 { - t.Fatal("Expected at least one bounce for this domain.") - } - - found := 0 - for _, bounce := range bounces { - t.Logf("Bounce Address: %s\n", bounce.Address) - if bounce.Address == exampleEmail { - found++ - } - } - - if found == 0 { + if !findBounce(exampleEmail) { t.Fatalf("Expected bounce for address %s in list of bounces", exampleEmail) } bounce, err := mg.GetBounce(ctx, exampleEmail) ensure.Nil(t, err) - if bounce.CreatedAt == "" { + if bounce.Address != exampleEmail { t.Fatalf("Expected at least one bounce for %s", exampleEmail) } + t.Logf("Bounce Created At: %s", bounce.CreatedAt) // Delete it. This should put us back the way we were. - err = mg.DeleteBounce(ctx, exampleEmail) ensure.Nil(t, err) // Make sure we're back to the way we were. - - bounces, err = mg.ListBounces(ctx, nil) - ensure.Nil(t, err) - - found = 0 - for _, bounce := range bounces { - t.Logf("Bounce Address: %s\n", bounce.Address) - if bounce.Address == exampleEmail { - found++ - } - } - - if found != 0 { - t.Fatalf("Expected no bounce for address %s in list of bounces", exampleEmail) + if findBounce(exampleEmail) { + t.Fatalf("Un-expected bounce for address %s in list of bounces", exampleEmail) } _, err = mg.GetBounce(ctx, exampleEmail) diff --git a/credentials.go b/credentials.go index f5e209d7..c5b61418 100644 --- a/credentials.go +++ b/credentials.go @@ -8,9 +8,9 @@ import ( // A Credential structure describes a principle allowed to send or receive mail at the domain. type Credential struct { - CreatedAt string `json:"created_at"` - Login string `json:"login"` - Password string `json:"password"` + CreatedAt RFC2822Time `json:"created_at"` + Login string `json:"login"` + Password string `json:"password"` } type credentialsListResponse struct { diff --git a/credentials_test.go b/credentials_test.go index df536f3b..c53b91e1 100644 --- a/credentials_test.go +++ b/credentials_test.go @@ -3,6 +3,7 @@ package mailgun import ( "context" "fmt" + "os" "strings" "testing" @@ -35,7 +36,7 @@ func TestCreateDeleteCredentials(t *testing.T) { t.Skip(reason) } - domain := reqEnv(t, "MG_DOMAIN") + domain := os.Getenv("MG_DOMAIN") mg, err := NewMailgunFromEnv() ensure.Nil(t, err) diff --git a/domains.go b/domains.go index 5c5e305b..af686e4a 100644 --- a/domains.go +++ b/domains.go @@ -4,7 +4,6 @@ import ( "context" "strconv" "strings" - "time" ) // DefaultLimit and DefaultSkip instruct the SDK to rely on Mailgun's reasonable defaults for Paging settings. @@ -27,11 +26,11 @@ type SpamAction string // A Domain structure holds information about a domain used when sending mail. type Domain struct { - CreatedAt string `json:"created_at"` - SMTPLogin string `json:"smtp_login"` - Name string `json:"name"` - SMTPPassword string `json:"smtp_password"` - Wildcard bool `json:"wildcard"` + CreatedAt RFC2822Time `json:"created_at"` + SMTPLogin string `json:"smtp_login"` + Name string `json:"name"` + SMTPPassword string `json:"smtp_password"` + Wildcard bool `json:"wildcard"` // The SpamAction field must be one of Tag, Disabled, or Delete. SpamAction string `json:"spam_action"` State string `json:"state"` @@ -83,12 +82,6 @@ type domainTrackingResponse struct { Tracking DomainTracking `json:"tracking"` } -// GetCreatedAt returns the time the domain was created as a normal Go time.Time type. -func (d Domain) GetCreatedAt() (t time.Time, err error) { - t, err = parseMailgunTime(d.CreatedAt) - return -} - // ListDomains retrieves a set of domains from Mailgun. // // Assuming no error, both the number of items retrieved and a slice of Domain instances. diff --git a/mailgun.go b/mailgun.go index 5a645e9e..c38cd524 100644 --- a/mailgun.go +++ b/mailgun.go @@ -148,12 +148,12 @@ type Mailgun interface { NewMessage(from, subject, text string, to ...string) *Message NewMIMEMessage(body io.ReadCloser, to ...string) *Message - ListBounces(ctx context.Context, opts *ListOptions) ([]Bounce, error) + ListBounces(opts *ListOptions) *BouncesIterator GetBounce(ctx context.Context, address string) (Bounce, error) AddBounce(ctx context.Context, address, code, error string) error DeleteBounce(ctx context.Context, address string) error - ListStats(ctx context.Context, events []string, opts *ListStatOptions) ([]Stats, error) + GetStats(ctx context.Context, events []string, opts *ListStatOptions) ([]Stats, error) GetTag(ctx context.Context, tag string) (Tag, error) DeleteTag(ctx context.Context, tag string) error ListTags(*ListTagOptions) *TagIterator diff --git a/mailgun_test.go b/mailgun_test.go index 62aa4236..27d00077 100644 --- a/mailgun_test.go +++ b/mailgun_test.go @@ -2,7 +2,6 @@ package mailgun import ( "net/http" - "strconv" "testing" "github.com/facebookgo/ensure" @@ -22,39 +21,3 @@ func TestMailgun(t *testing.T) { m.SetClient(client) ensure.DeepEqual(t, client, m.Client()) } - -func TestBounceGetCode(t *testing.T) { - b1 := &Bounce{ - CreatedAt: "blah", - Code: 123, - Address: "blort", - Error: "bletch", - } - c, err := b1.GetCode() - ensure.Nil(t, err) - ensure.DeepEqual(t, c, 123) - - b2 := &Bounce{ - CreatedAt: "blah", - Code: "456", - Address: "blort", - Error: "Bletch", - } - c, err = b2.GetCode() - ensure.Nil(t, err) - ensure.DeepEqual(t, c, 456) - - b3 := &Bounce{ - CreatedAt: "blah", - Code: "456H", - Address: "blort", - Error: "Bletch", - } - c, err = b3.GetCode() - ensure.NotNil(t, err) - - e, ok := err.(*strconv.NumError) - if !ok && e != nil { - t.Fatal("Expected a syntax error in numeric conversion: got ", err) - } -} diff --git a/mailing_lists.go b/mailing_lists.go index d914b0ef..f401bb5f 100644 --- a/mailing_lists.go +++ b/mailing_lists.go @@ -22,12 +22,12 @@ const ( // // AccessLevel may be one of ReadOnly, Members, or Everyone. type MailingList struct { - Address string `json:"address,omitempty"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - AccessLevel string `json:"access_level,omitempty"` - CreatedAt string `json:"created_at,omitempty"` - MembersCount int `json:"members_count,omitempty"` + Address string `json:"address,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + AccessLevel string `json:"access_level,omitempty"` + CreatedAt RFC2822Time `json:"created_at,omitempty"` + MembersCount int `json:"members_count,omitempty"` } type listsResponse struct { diff --git a/members.go b/members.go index c1ca0219..739eb0a7 100644 --- a/members.go +++ b/members.go @@ -52,7 +52,7 @@ type MemberListIterator struct { type ListOptions struct { Limit int - Skip int + Skip int } func (mg *MailgunImpl) ListMembers(address string, opts *ListOptions) *MemberListIterator { diff --git a/messages_test.go b/messages_test.go index a05d030d..a2741fe6 100644 --- a/messages_test.go +++ b/messages_test.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "os" "strings" "testing" "time" @@ -92,7 +93,7 @@ func TestSendMGPlain(t *testing.T) { } spendMoney(t, func() { - toUser := reqEnv(t, "MG_EMAIL_TO") + toUser := os.Getenv("MG_EMAIL_TO") mg, err := NewMailgunFromEnv() ensure.Nil(t, err) @@ -110,7 +111,7 @@ func TestSendMGPlainWithTracking(t *testing.T) { } spendMoney(t, func() { - toUser := reqEnv(t, "MG_EMAIL_TO") + toUser := os.Getenv("MG_EMAIL_TO") mg, err := NewMailgunFromEnv() ensure.Nil(t, err) @@ -129,7 +130,7 @@ func TestSendMGPlainAt(t *testing.T) { } spendMoney(t, func() { - toUser := reqEnv(t, "MG_EMAIL_TO") + toUser := os.Getenv("MG_EMAIL_TO") mg, err := NewMailgunFromEnv() ensure.Nil(t, err) @@ -148,7 +149,7 @@ func TestSendMGHtml(t *testing.T) { } spendMoney(t, func() { - toUser := reqEnv(t, "MG_EMAIL_TO") + toUser := os.Getenv("MG_EMAIL_TO") mg, err := NewMailgunFromEnv() ensure.Nil(t, err) @@ -167,7 +168,7 @@ func TestSendMGTracking(t *testing.T) { } spendMoney(t, func() { - toUser := reqEnv(t, "MG_EMAIL_TO") + toUser := os.Getenv("MG_EMAIL_TO") mg, err := NewMailgunFromEnv() ensure.Nil(t, err) @@ -186,7 +187,7 @@ func TestSendMGTag(t *testing.T) { } spendMoney(t, func() { - toUser := reqEnv(t, "MG_EMAIL_TO") + toUser := os.Getenv("MG_EMAIL_TO") mg, err := NewMailgunFromEnv() ensure.Nil(t, err) @@ -207,7 +208,7 @@ func TestSendMGMIME(t *testing.T) { } spendMoney(t, func() { - toUser := reqEnv(t, "MG_EMAIL_TO") + toUser := os.Getenv("MG_EMAIL_TO") mg, err := NewMailgunFromEnv() ensure.Nil(t, err) @@ -225,7 +226,7 @@ func TestSendMGBatchFailRecipients(t *testing.T) { } spendMoney(t, func() { - toUser := reqEnv(t, "MG_EMAIL_TO") + toUser := os.Getenv("MG_EMAIL_TO") mg, err := NewMailgunFromEnv() ensure.Nil(t, err) @@ -246,7 +247,7 @@ func TestSendMGBatchRecipientVariables(t *testing.T) { } spendMoney(t, func() { - toUser := reqEnv(t, "MG_EMAIL_TO") + toUser := os.Getenv("MG_EMAIL_TO") mg, err := NewMailgunFromEnv() ensure.Nil(t, err) diff --git a/mock_domains.go b/mock_domains.go index 2f1cdeb0..a5bab13d 100644 --- a/mock_domains.go +++ b/mock_domains.go @@ -20,7 +20,7 @@ func (ms *MockServer) addDomainRoutes(r chi.Router) { ms.domainList = append(ms.domainList, domainContainer{ Domain: Domain{ - CreatedAt: "Wed, 10 Jul 2013 19:26:52 GMT", + CreatedAt: RFC2822Time(time.Now().UTC()), Name: "mailgun.test", SMTPLogin: "postmaster@mailgun.test", SMTPPassword: "4rtqo4p6rrx9", @@ -122,10 +122,9 @@ func (ms *MockServer) getDomain(w http.ResponseWriter, r *http.Request) { } func (ms *MockServer) createDomain(w http.ResponseWriter, r *http.Request) { - now := time.Now() ms.domainList = append(ms.domainList, domainContainer{ Domain: Domain{ - CreatedAt: formatMailgunTime(&now), + CreatedAt: RFC2822Time(time.Now()), Name: r.FormValue("name"), SMTPLogin: r.FormValue("smtp_login"), SMTPPassword: r.FormValue("smtp_password"), diff --git a/mock_mailing_list.go b/mock_mailing_list.go index c4f80458..57ce15d3 100644 --- a/mock_mailing_list.go +++ b/mock_mailing_list.go @@ -32,7 +32,7 @@ func (ms *MockServer) addMailingListRoutes(r chi.Router) { MailingList: MailingList{ AccessLevel: "everyone", Address: "foo@mailgun.test", - CreatedAt: "Tue, 06 Mar 2012 05:44:45 GMT", + CreatedAt: RFC2822Time(time.Now().UTC()), Description: "Mailgun developers list", MembersCount: 1, Name: "", @@ -143,10 +143,9 @@ func (ms *MockServer) updateMailingList(w http.ResponseWriter, r *http.Request) } func (ms *MockServer) createMailingList(w http.ResponseWriter, r *http.Request) { - now := time.Now() ms.mailingList = append(ms.mailingList, mailingListContainer{ MailingList: MailingList{ - CreatedAt: formatMailgunTime(&now), + CreatedAt: RFC2822Time(time.Now().UTC()), Name: r.FormValue("name"), Address: r.FormValue("address"), Description: r.FormValue("description"), diff --git a/mock_routes.go b/mock_routes.go index 6615d45e..0b699f35 100644 --- a/mock_routes.go +++ b/mock_routes.go @@ -81,9 +81,8 @@ func (ms *MockServer) createRoute(w http.ResponseWriter, r *http.Request) { return } - now := time.Now() ms.routeList = append(ms.routeList, Route{ - CreatedAt: formatMailgunTime(&now), + CreatedAt: RFC2822Time(time.Now().UTC()), ID: randomString(10, "ID-"), Priority: stringToInt(r.FormValue("priority")), Description: r.FormValue("description"), diff --git a/rfc2822.go b/rfc2822.go new file mode 100644 index 00000000..e511d75e --- /dev/null +++ b/rfc2822.go @@ -0,0 +1,52 @@ +package mailgun + +import ( + "strconv" + "strings" + "time" +) + +// Mailgun uses RFC2822 format for timestamps everywhere ('Thu, 13 Oct 2011 18:02:00 GMT'), but +// by default Go's JSON package uses another format when decoding/encoding timestamps. +type RFC2822Time time.Time + +func NewRFC2822Time(str string) (RFC2822Time, error) { + t, err := time.Parse(time.RFC1123, str) + if err != nil { + return RFC2822Time{}, err + } + return RFC2822Time(t), nil +} + +func (t RFC2822Time) Unix() int64 { + return time.Time(t).Unix() +} + +func (t RFC2822Time) IsZero() bool { + return time.Time(t).IsZero() +} + +func (t RFC2822Time) MarshalJSON() ([]byte, error) { + return []byte(strconv.Quote(time.Time(t).Format(time.RFC1123))), nil +} + +func (t *RFC2822Time) UnmarshalJSON(s []byte) error { + q, err := strconv.Unquote(string(s)) + if err != nil { + return err + } + if *(*time.Time)(t), err = time.Parse(time.RFC1123, q); err != nil { + if strings.Contains(err.Error(), "extra text") { + if *(*time.Time)(t), err = time.Parse(time.RFC1123Z, q); err != nil { + return err + } + return nil + } + return err + } + return nil +} + +func (t RFC2822Time) String() string { + return time.Time(t).Format(time.RFC1123) +} diff --git a/routes.go b/routes.go index c76dad3c..6fc108a0 100644 --- a/routes.go +++ b/routes.go @@ -23,7 +23,7 @@ type Route struct { Actions []string `json:"actions,omitempty"` // The CreatedAt field provides a time-stamp for when the route came into existence. - CreatedAt string `json:"created_at,omitempty"` + CreatedAt RFC2822Time `json:"created_at,omitempty"` // ID field provides a unique identifier for this route. ID string `json:"id,omitempty"` } diff --git a/spam_complaints.go b/spam_complaints.go index 72cc95ba..81dc922b 100644 --- a/spam_complaints.go +++ b/spam_complaints.go @@ -14,9 +14,9 @@ const ( // Count provides a running counter of how many times // the recipient thought your messages were not solicited. type Complaint struct { - Count int `json:"count"` - CreatedAt string `json:"created_at"` - Address string `json:"address"` + Count int `json:"count"` + CreatedAt RFC2822Time `json:"created_at"` + Address string `json:"address"` } type complaintsResponse struct { diff --git a/spam_complaints_test.go b/spam_complaints_test.go index 9a9f416e..b82ed5b1 100644 --- a/spam_complaints_test.go +++ b/spam_complaints_test.go @@ -44,7 +44,6 @@ func TestGetComplaintFromRandomNoComplaint(t *testing.T) { } func TestCreateDeleteComplaint(t *testing.T) { - Debug = true if reason := SkipNetworkTest(); reason != "" { t.Skip(reason) } diff --git a/stats.go b/stats.go index 4487c2c1..d2439467 100644 --- a/stats.go +++ b/stats.go @@ -63,23 +63,22 @@ type statsTotalResponse struct { type Resolution string const ( - ResolutionHour = Resolution("hour") - ResolutionDay = Resolution("day") + ResolutionHour = Resolution("hour") + ResolutionDay = Resolution("day") ResolutionMonth = Resolution("month") ) type ListStatOptions struct { Resolution Resolution - Duration string - Start time.Time - End time.Time - Limit int - Skip int + Duration string + Start time.Time + End time.Time + Limit int + Skip int } // Returns total stats for a given domain for the specified time period -func (mg *MailgunImpl) ListStats(ctx context.Context, events []string, opts *ListStatOptions) ([]Stats, error) { - // TODO: Test this +func (mg *MailgunImpl) GetStats(ctx context.Context, events []string, opts *ListStatOptions) ([]Stats, error) { r := newHTTPRequest(generateApiUrl(mg, statsTotalEndpoint)) if opts != nil { diff --git a/stats_test.go b/stats_test.go index 0dec0a1c..ef7661b3 100644 --- a/stats_test.go +++ b/stats_test.go @@ -16,7 +16,7 @@ func TestListStats(t *testing.T) { ensure.Nil(t, err) ctx := context.Background() - stats, err := mg.ListStats(ctx, []string{"accepted", "delivered"}, nil) + stats, err := mg.GetStats(ctx, []string{"accepted", "delivered"}, nil) ensure.Nil(t, err) if len(stats) > 0 { diff --git a/unsubscribes.go b/unsubscribes.go index ec2eb1c6..9d1a61a4 100644 --- a/unsubscribes.go +++ b/unsubscribes.go @@ -6,10 +6,10 @@ import ( ) type Unsubscribe struct { - CreatedAt string `json:"created_at"` - Tags []string `json:"tags"` - ID string `json:"id"` - Address string `json:"address"` + CreatedAt RFC2822Time `json:"created_at"` + Tags []string `json:"tags"` + ID string `json:"id"` + Address string `json:"address"` } type unsubscribesResponse struct { @@ -19,24 +19,6 @@ type unsubscribesResponse struct { // Fetches the list of unsubscribes func (mg *MailgunImpl) ListUnsubscribes(opts *ListOptions) *UnsubscribesIterator { - /*r := newHTTPRequest(generateApiUrl(mg, unsubscribesEndpoint)) - r.setBasicAuth(basicAuthUser, mg.APIKey()) - r.setClient(mg.Client()) - - if opts != nil && opts.Limit != 0 { - r.addParameter("limit", strconv.Itoa(opts.Limit)) - } - - if opts != nil && opts.Skip != 0 { - r.addParameter("skip", strconv.Itoa(opts.Skip)) - } - - var envelope struct { - TotalCount int `json:"total_count"` - Items []Unsubscribe `json:"items"` - } - err := getResponseFromJSON(ctx, r, &envelope) - return envelope.Items, err*/ r := newHTTPRequest(generateApiUrl(mg, unsubscribesEndpoint)) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.APIKey()) diff --git a/unsubscribes_test.go b/unsubscribes_test.go index 6a090a67..4a918f44 100644 --- a/unsubscribes_test.go +++ b/unsubscribes_test.go @@ -2,6 +2,7 @@ package mailgun import ( "context" + "os" "testing" "github.com/facebookgo/ensure" @@ -12,7 +13,7 @@ func TestCreateUnsubscriber(t *testing.T) { t.Skip(reason) } - email := randomEmail("unsubcribe", reqEnv(t, "MG_DOMAIN")) + email := randomEmail("unsubcribe", os.Getenv("MG_DOMAIN")) mg, err := NewMailgunFromEnv() ensure.Nil(t, err) ctx := context.Background() @@ -21,7 +22,7 @@ func TestCreateUnsubscriber(t *testing.T) { ensure.Nil(t, mg.CreateUnsubscribe(ctx, email, "*")) } -func TestGetUnsubscribes(t *testing.T) { +func TestListUnsubscribes(t *testing.T) { if reason := SkipNetworkTest(); reason != "" { t.Skip(reason) } @@ -49,7 +50,7 @@ func TestGetUnsubscribe(t *testing.T) { t.Skip(reason) } - email := randomEmail("unsubcribe", reqEnv(t, "MG_DOMAIN")) + email := randomEmail("unsubcribe", os.Getenv("MG_DOMAIN")) mg, err := NewMailgunFromEnv() ensure.Nil(t, err) ctx := context.Background() @@ -61,14 +62,6 @@ func TestGetUnsubscribe(t *testing.T) { ensure.Nil(t, err) t.Logf("%s\t%s\t%s\t%s\t\n", u.ID, u.Address, u.CreatedAt, u.Tags) - /*t.Logf("Received %d unsubscribe records.\n", len(us)) - if len(us) > 0 { - t.Log("ID\tAddress\tCreated At\tTags\t") - for _, u := range us { - t.Logf("%s\t%s\t%s\t%s\t\n", u.ID, u.Address, u.CreatedAt, u.Tags) - } - } - */ // Destroy the unsubscription record ensure.Nil(t, mg.DeleteUnsubscribe(ctx, email)) } @@ -78,7 +71,7 @@ func TestCreateDestroyUnsubscription(t *testing.T) { t.Skip(reason) } - email := randomEmail("unsubcribe", reqEnv(t, "MG_DOMAIN")) + email := randomEmail("unsubcribe", os.Getenv("MG_DOMAIN")) mg, err := NewMailgunFromEnv() ensure.Nil(t, err)