Repository: mailgun/mailgun-go (go)

Issue:
Bulk upload Unsubscribes is not implemented
Per the [Mailgun documentation](https://documentation.mailgun.com/en/latest/api-suppressions.html#add-multiple-unsubscribes), the Mailgun API supports uploading up to 1000 unsubscribes at once but this is not implemented yet in mailgun-go. Is there a plan to implement this in the near future? I am happy to open a PR for it as long as it will be reviewed 🙂 

Pull request description:
Implement bulk unsubscribes creation.
resolves #233 

Patch:
diff --git a/examples/examples.go b/examples/examples.go
index 75b2eb7..d928b04 100644
--- a/examples/examples.go
+++ b/examples/examples.go
@@ -101,6 +101,20 @@ func CreateUnsubscribe(domain, apiKey string) error {
 	return mg.CreateUnsubscribe(ctx, "bob@example.com", "*")
 }
 
+func CreateUnsubscribes(domain, apiKey string) error {
+	mg := mailgun.NewMailgun(domain, apiKey)
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
+	defer cancel()
+
+	unsubscribes := []mailgun.Unsubscribe{
+		{Address: "alice@example.com"},
+		{Address: "bob@example.com", Tags: []string{"tag1"}},
+	}
+
+	return mg.CreateUnsubscribes(ctx, unsubscribes)
+}
+
 func CreateUnsubscribeWithTag(domain, apiKey string) error {
 	mg := mailgun.NewMailgun(domain, apiKey)
 
diff --git a/httphelpers.go b/httphelpers.go
index a74adc6..382659b 100644
--- a/httphelpers.go
+++ b/httphelpers.go
@@ -68,6 +68,10 @@ type urlEncodedPayload struct {
 	Values []keyValuePair
 }
 
+type jsonEncodedPayload struct {
+	payload interface{}
+}
+
 func newHTTPRequest(url string) *httpRequest {
 	return &httpRequest{URL: url, Client: http.DefaultClient}
 }
@@ -88,6 +92,27 @@ func (r *httpRequest) setBasicAuth(user, password string) {
 	r.BasicAuthPassword = password
 }
 
+func newJSONEncodedPayload(payload interface{}) *jsonEncodedPayload {
+	return &jsonEncodedPayload{payload: payload}
+}
+
+func (j *jsonEncodedPayload) getPayloadBuffer() (*bytes.Buffer, error) {
+	b, err := json.Marshal(j.payload)
+	if err != nil {
+		return nil, err
+	}
+
+	return bytes.NewBuffer(b), nil
+}
+
+func (j *jsonEncodedPayload) getContentType() string {
+	return "application/json"
+}
+
+func (j *jsonEncodedPayload) getValues() []keyValuePair {
+	return nil
+}
+
 func newUrlEncodedPayload() *urlEncodedPayload {
 	return &urlEncodedPayload{}
 }
@@ -322,8 +347,16 @@ func (r *httpRequest) curlString(req *http.Request, p payload) string {
 	//parts = append(parts, fmt.Sprintf(" --user '%s:%s'", r.BasicAuthUser, r.BasicAuthPassword))
 
 	if p != nil {
-		for _, param := range p.getValues() {
-			parts = append(parts, fmt.Sprintf(" -F %s='%s'", param.key, param.value))
+		if p.getContentType() == "application/json" {
+			b, err := p.getPayloadBuffer()
+			if err != nil {
+				return "Unable to get payload buffer: " + err.Error()
+			}
+			parts = append(parts, fmt.Sprintf("--data '%s'", b.String()))
+		} else {
+			for _, param := range p.getValues() {
+				parts = append(parts, fmt.Sprintf(" -F %s='%s'", param.key, param.value))
+			}
 		}
 	}
 	return strings.Join(parts, " ")
diff --git a/mailgun.go b/mailgun.go
index f4bc7ea..16be275 100644
--- a/mailgun.go
+++ b/mailgun.go
@@ -171,6 +171,7 @@ type Mailgun interface {
 	ListUnsubscribes(opts *ListOptions) *UnsubscribesIterator
 	GetUnsubscribe(ctx context.Context, address string) (Unsubscribe, error)
 	CreateUnsubscribe(ctx context.Context, address, tag string) error
+	CreateUnsubscribes(ctx context.Context, unsubscribes []Unsubscribe) error
 	DeleteUnsubscribe(ctx context.Context, address string) error
 	DeleteUnsubscribeWithTag(ctx context.Context, a, t string) error
 
diff --git a/unsubscribes.go b/unsubscribes.go
index fd81a07..5e6e7f1 100644
--- a/unsubscribes.go
+++ b/unsubscribes.go
@@ -6,9 +6,9 @@ import (
 )
 
 type Unsubscribe struct {
-	CreatedAt RFC2822Time `json:"created_at"`
-	Tags      []string    `json:"tags"`
-	ID        string      `json:"id"`
+	CreatedAt RFC2822Time `json:"created_at,omitempty"`
+	Tags      []string    `json:"tags,omitempty"`
+	ID        string      `json:"id,omitempty"`
 	Address   string      `json:"address"`
 }
 
@@ -157,6 +157,18 @@ func (mg *MailgunImpl) CreateUnsubscribe(ctx context.Context, address, tag strin
 	return err
 }
 
+// CreateUnsubscribes adds multiple e-mail addresses to the domain's unsubscription table.
+func (mg *MailgunImpl) CreateUnsubscribes(ctx context.Context, unsubscribes []Unsubscribe) error {
+	r := newHTTPRequest(generateApiUrl(mg, unsubscribesEndpoint))
+	r.setClient(mg.Client())
+	r.setBasicAuth(basicAuthUser, mg.APIKey())
+	r.addHeader("Content-Type", "application/json")
+
+	p := newJSONEncodedPayload(unsubscribes)
+	_, err := makePostRequest(ctx, r, p)
+	return err
+}
+
 // DeleteUnsubscribe removes the e-mail address given from the domain's unsubscription table.
 // If passing in an ID (discoverable from, e.g., ListUnsubscribes()), the e-mail address associated
 // with the given ID will be removed.


Test patch:
diff --git a/unsubscribes_test.go b/unsubscribes_test.go
index 4a918f4..ecb4a1f 100644
--- a/unsubscribes_test.go
+++ b/unsubscribes_test.go
@@ -22,6 +22,28 @@ func TestCreateUnsubscriber(t *testing.T) {
 	ensure.Nil(t, mg.CreateUnsubscribe(ctx, email, "*"))
 }
 
+func TestCreateUnsubscribes(t *testing.T) {
+	if reason := SkipNetworkTest(); reason != "" {
+		t.Skip(reason)
+	}
+
+	unsubscribes := []Unsubscribe{
+		{
+			Address: randomEmail("unsubcribe", os.Getenv("MG_DOMAIN")),
+		},
+		{
+			Address: randomEmail("unsubcribe", os.Getenv("MG_DOMAIN")),
+			Tags:    []string{"tag1"},
+		},
+	}
+	mg, err := NewMailgunFromEnv()
+	ensure.Nil(t, err)
+	ctx := context.Background()
+
+	// Create unsubscription records
+	ensure.Nil(t, mg.CreateUnsubscribes(ctx, unsubscribes))
+}
+
 func TestListUnsubscribes(t *testing.T) {
 	if reason := SkipNetworkTest(); reason != "" {
 		t.Skip(reason)