diff --git a/domains.go b/domains.go index 6565ec7..5c5e305 100644 --- a/domains.go +++ b/domains.go @@ -3,6 +3,7 @@ package mailgun import ( "context" "strconv" + "strings" "time" ) @@ -17,11 +18,13 @@ const ( // Tag instruments the received message with headers providing a measure of its spamness. // Delete instructs Mailgun to just block or delete the message all-together. const ( - SpamActionTag = "tag" - SpamActionDisabled = "disabled" - SpamActionDelete = "delete" + SpamActionTag = SpamAction("tag") + SpamActionDisabled = SpamAction("disabled") + SpamActionDelete = SpamAction("delete") ) +type SpamAction string + // A Domain structure holds information about a domain used when sending mail. type Domain struct { CreatedAt string `json:"created_at"` @@ -30,8 +33,8 @@ type Domain struct { 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"` + SpamAction string `json:"spam_action"` + State string `json:"state"` } // DNSRecord structures describe intended records to properly configure your domain for use with Mailgun. @@ -123,22 +126,46 @@ func (mg *MailgunImpl) GetDomain(ctx context.Context, domain string) (Domain, [] return resp.Domain, resp.ReceivingDNSRecords, resp.SendingDNSRecords, err } +type CreateDomainOptions struct { + SpamAction SpamAction + Wildcard bool + ForceDKIMAuthority bool + DKIMKeySize int + IPS []string +} + // CreateDomain instructs Mailgun to create a new domain for your account. // The name parameter identifies the domain. // The smtpPassword parameter provides an access credential for the domain. // The spamAction domain must be one of Delete, Tag, or Disabled. // The wildcard parameter instructs Mailgun to treat all subdomains of this domain uniformly if true, // and as different domains if false. -func (mg *MailgunImpl) CreateDomain(ctx context.Context, name string, smtpPassword string, spamAction string, wildcard bool) error { +func (mg *MailgunImpl) CreateDomain(ctx context.Context, name string, password string, opts *CreateDomainOptions) error { r := newHTTPRequest(generatePublicApiUrl(mg, domainsEndpoint)) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.APIKey()) payload := newUrlEncodedPayload() payload.addValue("name", name) - payload.addValue("smtp_password", smtpPassword) - payload.addValue("spam_action", spamAction) - payload.addValue("wildcard", strconv.FormatBool(wildcard)) + payload.addValue("smtp_password", password) + + if opts != nil { + if opts.SpamAction != "" { + payload.addValue("spam_action", string(opts.SpamAction)) + } + if opts.Wildcard { + payload.addValue("wildcard", boolToString(opts.Wildcard)) + } + if opts.ForceDKIMAuthority { + payload.addValue("force_dkim_authority", boolToString(opts.ForceDKIMAuthority)) + } + if opts.DKIMKeySize != 0 { + payload.addValue("dkim_key_size", strconv.Itoa(opts.DKIMKeySize)) + } + if len(opts.IPS) != 0 { + payload.addValue("ips", strings.Join(opts.IPS, ",")) + } + } _, err := makePostRequest(ctx, r, payload) return err } diff --git a/domains_test.go b/domains_test.go index 37f0af3..9ab836e 100644 --- a/domains_test.go +++ b/domains_test.go @@ -71,7 +71,8 @@ func TestAddDeleteDomain(t *testing.T) { ctx := context.Background() // First, we need to add the domain. - ensure.Nil(t, mg.CreateDomain(ctx, "mx.mailgun.test", "supersecret", mailgun.SpamActionTag, false)) + ensure.Nil(t, mg.CreateDomain(ctx, "mx.mailgun.test", "supersecret", + &mailgun.CreateDomainOptions{SpamAction: mailgun.SpamActionTag})) // Next, we delete it. ensure.Nil(t, mg.DeleteDomain(ctx, "mx.mailgun.test")) } diff --git a/mailgun.go b/mailgun.go index eaea98c..6c40688 100644 --- a/mailgun.go +++ b/mailgun.go @@ -154,13 +154,13 @@ type Mailgun interface { DeleteBounce(ctx context.Context, address string) error ListStats(ctx context.Context, events []string, opts *ListStatOptions) ([]Stats, error) - GetTag(ctx context.Context, tag string) (TagItem, error) + GetTag(ctx context.Context, tag string) (Tag, error) DeleteTag(ctx context.Context, tag string) error ListTags(*ListTagOptions) *TagIterator ListDomains(ctx context.Context, opts *ListOptions) (int, []Domain, error) GetDomain(ctx context.Context, domain string) (Domain, []DNSRecord, []DNSRecord, error) - CreateDomain(ctx context.Context, name string, smtpPassword string, spamAction string, wildcard bool) error + CreateDomain(ctx context.Context, name string, pass string, opts *CreateDomainOptions) error DeleteDomain(ctx context.Context, name string) error UpdateDomainConnection(ctx context.Context, domain string, dc DomainConnection) error GetDomainConnection(ctx context.Context, domain string) (DomainConnection, error) @@ -183,7 +183,7 @@ type Mailgun interface { DeleteUnsubscribe(ctx context.Context, address string) error DeleteUnsubscribeWithTag(ctx context.Context, a, t string) error - ListComplaints(ctx context.Context, opts *ListOptions) ([]Complaint, error) + ListComplaints(opts *ListOptions) *ComplaintsIterator GetComplaint(ctx context.Context, address string) (Complaint, error) CreateComplaint(ctx context.Context, address string) error DeleteComplaint(ctx context.Context, address string) error diff --git a/mailing_lists.go b/mailing_lists.go index 1e23beb..d914b0e 100644 --- a/mailing_lists.go +++ b/mailing_lists.go @@ -79,7 +79,9 @@ func (li *ListsIterator) Next(ctx context.Context, items *[]MailingList) bool { if li.err != nil { return false } - *items = li.Items + cpy := make([]MailingList, len(li.Items)) + copy(cpy, li.Items) + *items = cpy if len(li.Items) == 0 { return false } @@ -97,7 +99,9 @@ func (li *ListsIterator) First(ctx context.Context, items *[]MailingList) bool { if li.err != nil { return false } - *items = li.Items + cpy := make([]MailingList, len(li.Items)) + copy(cpy, li.Items) + *items = cpy return true } @@ -113,7 +117,9 @@ func (li *ListsIterator) Last(ctx context.Context, items *[]MailingList) bool { if li.err != nil { return false } - *items = li.Items + cpy := make([]MailingList, len(li.Items)) + copy(cpy, li.Items) + *items = cpy return true } @@ -131,7 +137,9 @@ func (li *ListsIterator) Previous(ctx context.Context, items *[]MailingList) boo if li.err != nil { return false } - *items = li.Items + cpy := make([]MailingList, len(li.Items)) + copy(cpy, li.Items) + *items = cpy if len(li.Items) == 0 { return false } diff --git a/routes.go b/routes.go index 9901f13..00111bb 100644 --- a/routes.go +++ b/routes.go @@ -180,6 +180,8 @@ func (ri *RoutesIterator) Previous(ctx context.Context, items *[]Route) bool { func (ri *RoutesIterator) fetch(ctx context.Context, skip, limit int) error { r := newHTTPRequest(ri.url) + r.setBasicAuth(basicAuthUser, ri.mg.APIKey()) + r.setClient(ri.mg.Client()) if skip != 0 { r.addParameter("skip", strconv.Itoa(skip)) @@ -188,9 +190,6 @@ func (ri *RoutesIterator) fetch(ctx context.Context, skip, limit int) error { r.addParameter("limit", strconv.Itoa(limit)) } - r.setClient(ri.mg.Client()) - r.setBasicAuth(basicAuthUser, ri.mg.APIKey()) - return getResponseFromJSON(ctx, r, &ri.routesListResponse) } diff --git a/spam_complaints.go b/spam_complaints.go index 6167e8d..a67f82d 100644 --- a/spam_complaints.go +++ b/spam_complaints.go @@ -19,33 +19,126 @@ type Complaint struct { Address string `json:"address"` } -type complaintsEnvelope struct { - Items []Complaint `json:"items"` +type complaintsResponse struct { Paging Paging `json:"paging"` + Items []Complaint `json:"items"` } // ListComplaints returns a set of spam complaints registered against your domain. // Recipients of your messages can click on a link which sends feedback to Mailgun // indicating that the message they received is, to them, spam. -func (mg *MailgunImpl) ListComplaints(ctx context.Context, opts *ListOptions) ([]Complaint, error) { - r := newHTTPRequest(generateApiUrl(mg, complaintsEndpoint)) +func (mg *MailgunImpl) ListComplaints(opts *ListOptions) *ComplaintsIterator { + r := newHTTPRequest(generatePublicApiUrl(mg, mg.domain+"/"+complaintsEndpoint)) 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 &ComplaintsIterator{ + mg: mg, + complaintsResponse: complaintsResponse{Paging: Paging{Next: url, First: url}}, + err: err, + } +} - if opts != nil && opts.Limit != 0 { - r.addParameter("limit", strconv.Itoa(opts.Limit)) +type ComplaintsIterator struct { + complaintsResponse + mg Mailgun + err error +} + +// If an error occurred during iteration `Err()` will return non nil +func (ci *ComplaintsIterator) 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 *ComplaintsIterator) Next(ctx context.Context, items *[]Complaint) bool { + if ci.err != nil { + return false } + ci.err = ci.fetch(ctx, ci.Paging.Next) + if ci.err != nil { + return false + } + cpy := make([]Complaint, 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 *ComplaintsIterator) First(ctx context.Context, items *[]Complaint) bool { + if ci.err != nil { + return false } + ci.err = ci.fetch(ctx, ci.Paging.First) + if ci.err != nil { + return false + } + cpy := make([]Complaint, len(ci.Items)) + copy(cpy, ci.Items) + *items = cpy + return true +} + +// 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 *ComplaintsIterator) Last(ctx context.Context, items *[]Complaint) bool { + if ci.err != nil { + return false + } + ci.err = ci.fetch(ctx, ci.Paging.Last) + if ci.err != nil { + return false + } + cpy := make([]Complaint, len(ci.Items)) + copy(cpy, ci.Items) + *items = cpy + return true +} - var envelope complaintsEnvelope - err := getResponseFromJSON(ctx, r, &envelope) - 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 *ComplaintsIterator) Previous(ctx context.Context, items *[]Complaint) bool { + if ci.err != nil { + return false } - return envelope.Items, nil + if ci.Paging.Previous == "" { + return false + } + ci.err = ci.fetch(ctx, ci.Paging.Previous) + if ci.err != nil { + return false + } + cpy := make([]Complaint, len(ci.Items)) + copy(cpy, ci.Items) + *items = cpy + if len(ci.Items) == 0 { + return false + } + return true +} + +func (ci *ComplaintsIterator) fetch(ctx context.Context, url string) error { + r := newHTTPRequest(url) + r.setClient(ci.mg.Client()) + r.setBasicAuth(basicAuthUser, ci.mg.APIKey()) + + return getResponseFromJSON(ctx, r, &ci.complaintsResponse) } // GetComplaint returns a single complaint record filed by a recipient at the email address provided. diff --git a/spam_complaints_test.go b/spam_complaints_test.go index 6d31e37..9a9f416 100644 --- a/spam_complaints_test.go +++ b/spam_complaints_test.go @@ -18,7 +18,11 @@ func TestGetComplaints(t *testing.T) { ensure.Nil(t, err) ctx := context.Background() - _, err = mg.ListComplaints(ctx, nil) + it := mg.ListComplaints(nil) + var page []Complaint + for it.Next(ctx, &page) { + //spew.Dump(page) + } ensure.Nil(t, err) } @@ -40,6 +44,7 @@ func TestGetComplaintFromRandomNoComplaint(t *testing.T) { } func TestCreateDeleteComplaint(t *testing.T) { + Debug = true if reason := SkipNetworkTest(); reason != "" { t.Skip(reason) } @@ -50,13 +55,16 @@ func TestCreateDeleteComplaint(t *testing.T) { var hasComplaint = func(email string) bool { t.Logf("hasComplaint: %s\n", email) - complaints, err := mg.ListComplaints(ctx, nil) + it := mg.ListComplaints(nil) ensure.Nil(t, err) - for _, complaint := range complaints { - t.Logf("Complaint Address: %s\n", complaint.Address) - if complaint.Address == email { - return true + var page []Complaint + for it.Next(ctx, &page) { + for _, complaint := range page { + t.Logf("Complaint Address: %s\n", complaint.Address) + if complaint.Address == email { + return true + } } } return false diff --git a/tags.go b/tags.go index d9fec3c..42cc67b 100644 --- a/tags.go +++ b/tags.go @@ -7,7 +7,7 @@ import ( "time" ) -type TagItem struct { +type Tag struct { Value string `json:"tag"` Description string `json:"description"` FirstSeen *time.Time `json:"first-seen,omitempty"` @@ -15,8 +15,8 @@ type TagItem struct { } type tagsResponse struct { - Items []TagItem `json:"items"` - Paging Paging `json:"paging"` + Items []Tag `json:"items"` + Paging Paging `json:"paging"` } type ListTagOptions struct { @@ -40,11 +40,11 @@ func (mg *MailgunImpl) DeleteTag(ctx context.Context, tag string) error { } // GetTag retrieves metadata about the tag from the api -func (mg *MailgunImpl) GetTag(ctx context.Context, tag string) (TagItem, error) { +func (mg *MailgunImpl) GetTag(ctx context.Context, tag string) (Tag, error) { r := newHTTPRequest(generateApiUrl(mg, tagsEndpoint) + "/" + tag) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.APIKey()) - var tagItem TagItem + var tagItem Tag return tagItem, getResponseFromJSON(ctx, r, &tagItem) } @@ -79,19 +79,19 @@ func (mg *MailgunImpl) ListTags(opts *ListTagOptions) *TagIterator { url, err := req.generateUrlWithParameters() return &TagIterator{ tagsResponse: tagsResponse{Paging: Paging{Next: url, First: url}}, - err: err, - mg: mg, + err: err, + mg: mg, } } type TagIterator struct { tagsResponse - mg Mailgun - err error + mg Mailgun + err error } // Returns the next page in the list of tags -func (ti *TagIterator) Next(ctx context.Context, items *[]TagItem) bool { +func (ti *TagIterator) Next(ctx context.Context, items *[]Tag) bool { if ti.err != nil { return false } @@ -112,7 +112,7 @@ func (ti *TagIterator) Next(ctx context.Context, items *[]TagItem) bool { } // Returns the previous page in the list of tags -func (ti *TagIterator) Previous(ctx context.Context, items *[]TagItem) bool { +func (ti *TagIterator) Previous(ctx context.Context, items *[]Tag) bool { if ti.err != nil { return false } @@ -137,7 +137,7 @@ func (ti *TagIterator) Previous(ctx context.Context, items *[]TagItem) bool { } // Returns the first page in the list of tags -func (ti *TagIterator) First(ctx context.Context, items *[]TagItem) bool { +func (ti *TagIterator) First(ctx context.Context, items *[]Tag) bool { if ti.err != nil { return false } @@ -150,7 +150,7 @@ func (ti *TagIterator) First(ctx context.Context, items *[]TagItem) bool { } // Returns the last page in the list of tags -func (ti *TagIterator) Last(ctx context.Context, items *[]TagItem) bool { +func (ti *TagIterator) Last(ctx context.Context, items *[]Tag) bool { if ti.err != nil { return false } diff --git a/tags_test.go b/tags_test.go index 1924241..1e78768 100644 --- a/tags_test.go +++ b/tags_test.go @@ -36,13 +36,13 @@ func TestTags(t *testing.T) { ensure.Nil(t, err) // Wait for the tag to show up - ensure.Nil(t,waitForTag(mg, "newsletter")) + ensure.Nil(t, waitForTag(mg, "newsletter")) // Should return a list of available tags - it := mg.ListTags( nil) - var page []mailgun.TagItem + it := mg.ListTags(nil) + var page []mailgun.Tag for it.Next(ctx, &page) { - ensure.True(t, len(page)!= 0) + ensure.True(t, len(page) != 0) log.Printf("Tags: %+v\n", page) } ensure.Nil(t, it.Err()) @@ -50,7 +50,7 @@ func TestTags(t *testing.T) { // Should return a limited list of available tags cursor := mg.ListTags(&mailgun.ListTagOptions{Limit: 1}) - var tags []mailgun.TagItem + var tags []mailgun.Tag for cursor.Next(ctx, &tags) { ensure.DeepEqual(t, len(tags), 1) log.Printf("Tags: %+v\n", tags)