Repository: mailgun/mailgun-go
Pull request #288 (2022-05-27 13:53:01)

Added CaptureCurlOutput
## Purpose
Added global vars to allow users to capture curl output. These vars are NOT thread safe and are not intended to be changed during normal operation, but as optional debug output.

## Implementation
Moved some tests from `mailgun` package to `mailgun_test` package.


Patch:
diff --git a/exports.go b/exports.go
index 8513f50..d3c39a5 100644
--- a/exports.go
+++ b/exports.go
@@ -80,8 +80,13 @@ func (mg *MailgunImpl) GetExportLink(ctx context.Context, id string) (string, er
 	if err != nil {
 		return "", err
 	}
+
 	if Debug {
-		fmt.Println(r.curlString(req, nil))
+		if CaptureCurlOutput {
+			r.capturedCurlOutput = r.curlString(req, nil)
+		} else {
+			fmt.Println(r.curlString(req, nil))
+		}
 	}
 
 	resp, err := r.Client.Do(req)
diff --git a/httphelpers.go b/httphelpers.go
index a0249b7..3ff0d11 100644
--- a/httphelpers.go
+++ b/httphelpers.go
@@ -21,12 +21,13 @@ import (
 var validURL = regexp.MustCompile(`/v[2-4].*`)
 
 type httpRequest struct {
-	URL               string
-	Parameters        map[string][]string
-	Headers           map[string]string
-	BasicAuthUser     string
-	BasicAuthPassword string
-	Client            *http.Client
+	URL                string
+	Parameters         map[string][]string
+	Headers            map[string]string
+	BasicAuthUser      string
+	BasicAuthPassword  string
+	Client             *http.Client
+	capturedCurlOutput string
 }
 
 type httpResponse struct {
@@ -292,7 +293,11 @@ func (r *httpRequest) makeRequest(ctx context.Context, method string, payload pa
 	}
 
 	if Debug {
-		fmt.Println(r.curlString(req, payload))
+		if CaptureCurlOutput {
+			r.capturedCurlOutput = r.curlString(req, payload)
+		} else {
+			fmt.Println(r.curlString(req, payload))
+		}
 	}
 
 	response := httpResponse{}
@@ -347,7 +352,11 @@ func (r *httpRequest) curlString(req *http.Request, p payload) string {
 
 	parts := []string{"curl", "-i", "-X", req.Method, req.URL.String()}
 	for key, value := range req.Header {
-		parts = append(parts, fmt.Sprintf("-H \"%s: %s\"", key, value[0]))
+		if key == "Authorization" {
+			parts = append(parts, fmt.Sprintf("-H \"%s: %s\"", key, "<redacted>"))
+		} else {
+			parts = append(parts, fmt.Sprintf("-H \"%s: %s\"", key, value[0]))
+		}
 	}
 
 	//parts = append(parts, fmt.Sprintf(" --user '%s:%s'", r.BasicAuthUser, r.BasicAuthPassword))
diff --git a/mailgun.go b/mailgun.go
index e35e9b4..9ade586 100644
--- a/mailgun.go
+++ b/mailgun.go
@@ -85,9 +85,17 @@ import (
 	"time"
 )
 
-// Set true to write the HTTP requests in curl for to stdout
+// Debug set true to write the HTTP requests in curl for to stdout
 var Debug = false
 
+// CaptureCurlOutput if true, will capture the curl request of the last Send()
+// can be retrieved by calling GetCurlOutput()
+var CaptureCurlOutput = false
+
+// RedactCurlAuth will redact the authentication header when CaptureCurlOutput is
+// used.
+var RedactCurlAuth = false
+
 const (
 	// Base Url the library uses to contact mailgun. Use SetAPIBase() to override
 	APIBase              = "https://api.mailgun.net/v3"
@@ -126,6 +134,7 @@ type Mailgun interface {
 	SetClient(client *http.Client)
 	SetAPIBase(url string)
 	AddOverrideHeader(k string, v string)
+	GetCurlOutput() string
 
 	Send(ctx context.Context, m *Message) (string, string, error)
 	ReSend(ctx context.Context, id string, recipients ...string) (string, string, error)
@@ -242,12 +251,13 @@ type Mailgun interface {
 // MailgunImpl bundles data needed by a large number of methods in order to interact with the Mailgun API.
 // Colloquially, we refer to instances of this structure as "clients."
 type MailgunImpl struct {
-	apiBase         string
-	domain          string
-	apiKey          string
-	client          *http.Client
-	baseURL         string
-	overrideHeaders map[string]string
+	apiBase            string
+	domain             string
+	apiKey             string
+	client             *http.Client
+	baseURL            string
+	overrideHeaders    map[string]string
+	capturedCurlOutput string
 }
 
 // NewMailGun creates a new client instance.
@@ -329,6 +339,13 @@ func (mg *MailgunImpl) AddOverrideHeader(k string, v string) {
 	mg.overrideHeaders[k] = v
 }
 
+// GetCurlOutput will retrieve the output of the last Send() request as a curl command.
+// mailgun.CaptureCurlOutput must be set to true
+// This is mostly useful for testing the Mailgun API hosted at a different endpoint.
+func (mg *MailgunImpl) GetCurlOutput() string {
+	return mg.capturedCurlOutput
+}
+
 // generateApiUrl renders a URL for an API endpoint using the domain and endpoint name.
 func generateApiUrl(m Mailgun, endpoint string) string {
 	return fmt.Sprintf("%s/%s/%s", m.APIBase(), m.Domain(), endpoint)
diff --git a/messages.go b/messages.go
index 90cfca7..30e433d 100644
--- a/messages.go
+++ b/messages.go
@@ -684,6 +684,10 @@ func (mg *MailgunImpl) Send(ctx context.Context, message *Message) (mes string,
 		id = response.Id
 	}
 
+	if r.capturedCurlOutput != "" {
+		mg.capturedCurlOutput = r.capturedCurlOutput
+	}
+
 	return
 }
 


Test patch:
diff --git a/messages_test.go b/messages_test.go
index 9f2ad8b..112f62f 100644
--- a/messages_test.go
+++ b/messages_test.go
@@ -1,4 +1,4 @@
-package mailgun
+package mailgun_test
 
 import (
 	"context"
@@ -12,12 +12,10 @@ import (
 	"time"
 
 	"github.com/facebookgo/ensure"
+	"github.com/mailgun/mailgun-go/v4"
 )
 
 const (
-	fromUser       = "=?utf-8?q?Katie_Brewer=2C_CFP=C2=AE?= <joe@example.com>"
-	exampleSubject = "Mailgun-go Example Subject"
-	exampleText    = "Testing some Mailgun awesomeness!\nhttps://www.mailgun.com/"
 	exampleHtml    = "<html><head /><body><p>Testing some <a href=\"http://google.com?q=abc&r=def&s=ghi\">Mailgun HTML awesomeness!</a> at www.kc5tja@yahoo.com</p></body></html>"
 	exampleAMPHtml = `<!doctype html><html ⚡4email><head><meta charset="utf-8"><script async src="https://cdn.ampproject.org/v0.js"></script><style amp4email-boilerplate>body{visibility:hidden}</style><style amp-custom>h1{margin: 1rem;}</style></head><body><h1>Hello, I am an AMP EMAIL!</h1></body></html>`
 	exampleMime    = `Content-Type: text/plain; charset="ascii"
@@ -34,14 +32,29 @@ Testing some Mailgun MIME awesomeness!
 	exampleAPIKey = "testAPIKey"
 )
 
+func init() {
+	mailgun.Debug = true
+	mailgun.CaptureCurlOutput = true
+	mailgun.RedactCurlAuth = true
+}
+
+func spendMoney(t *testing.T, tFunc func()) {
+	ok := os.Getenv("MG_SPEND_MONEY")
+	if ok != "" {
+		tFunc()
+	} else {
+		t.Log("Money spending not allowed, not running function.")
+	}
+}
+
 func TestSendMGPlain(t *testing.T) {
-	if reason := SkipNetworkTest(); reason != "" {
+	if reason := mailgun.SkipNetworkTest(); reason != "" {
 		t.Skip(reason)
 	}
 
 	spendMoney(t, func() {
 		toUser := os.Getenv("MG_EMAIL_TO")
-		mg, err := NewMailgunFromEnv()
+		mg, err := mailgun.NewMailgunFromEnv()
 		ensure.Nil(t, err)
 
 		ctx := context.Background()
@@ -53,13 +66,13 @@ func TestSendMGPlain(t *testing.T) {
 }
 
 func TestSendMGPlainWithTracking(t *testing.T) {
-	if reason := SkipNetworkTest(); reason != "" {
+	if reason := mailgun.SkipNetworkTest(); reason != "" {
 		t.Skip(reason)
 	}
 
 	spendMoney(t, func() {
 		toUser := os.Getenv("MG_EMAIL_TO")
-		mg, err := NewMailgunFromEnv()
+		mg, err := mailgun.NewMailgunFromEnv()
 		ensure.Nil(t, err)
 
 		ctx := context.Background()
@@ -72,13 +85,13 @@ func TestSendMGPlainWithTracking(t *testing.T) {
 }
 
 func TestSendMGPlainAt(t *testing.T) {
-	if reason := SkipNetworkTest(); reason != "" {
+	if reason := mailgun.SkipNetworkTest(); reason != "" {
 		t.Skip(reason)
 	}
 
 	spendMoney(t, func() {
 		toUser := os.Getenv("MG_EMAIL_TO")
-		mg, err := NewMailgunFromEnv()
+		mg, err := mailgun.NewMailgunFromEnv()
 		ensure.Nil(t, err)
 
 		ctx := context.Background()
@@ -91,13 +104,13 @@ func TestSendMGPlainAt(t *testing.T) {
 }
 
 func TestSendMGHtml(t *testing.T) {
-	if reason := SkipNetworkTest(); reason != "" {
+	if reason := mailgun.SkipNetworkTest(); reason != "" {
 		t.Skip(reason)
 	}
 
 	spendMoney(t, func() {
 		toUser := os.Getenv("MG_EMAIL_TO")
-		mg, err := NewMailgunFromEnv()
+		mg, err := mailgun.NewMailgunFromEnv()
 		ensure.Nil(t, err)
 
 		ctx := context.Background()
@@ -110,13 +123,13 @@ func TestSendMGHtml(t *testing.T) {
 }
 
 func TestSendMGAMPHtml(t *testing.T) {
-	if reason := SkipNetworkTest(); reason != "" {
+	if reason := mailgun.SkipNetworkTest(); reason != "" {
 		t.Skip(reason)
 	}
 
 	spendMoney(t, func() {
 		toUser := os.Getenv("MG_EMAIL_TO")
-		mg, err := NewMailgunFromEnv()
+		mg, err := mailgun.NewMailgunFromEnv()
 		ensure.Nil(t, err)
 
 		ctx := context.Background()
@@ -130,13 +143,13 @@ func TestSendMGAMPHtml(t *testing.T) {
 }
 
 func TestSendMGTracking(t *testing.T) {
-	if reason := SkipNetworkTest(); reason != "" {
+	if reason := mailgun.SkipNetworkTest(); reason != "" {
 		t.Skip(reason)
 	}
 
 	spendMoney(t, func() {
 		toUser := os.Getenv("MG_EMAIL_TO")
-		mg, err := NewMailgunFromEnv()
+		mg, err := mailgun.NewMailgunFromEnv()
 		ensure.Nil(t, err)
 
 		ctx := context.Background()
@@ -149,19 +162,19 @@ func TestSendMGTracking(t *testing.T) {
 }
 
 func TestSendMGTrackingClicksHtmlOnly(t *testing.T) {
-	if reason := SkipNetworkTest(); reason != "" {
+	if reason := mailgun.SkipNetworkTest(); reason != "" {
 		t.Skip(reason)
 	}
 
 	spendMoney(t, func() {
 		toUser := os.Getenv("MG_EMAIL_TO")
-		mg, err := NewMailgunFromEnv()
+		mg, err := mailgun.NewMailgunFromEnv()
 		ensure.Nil(t, err)
 
 		ctx := context.Background()
 		m := mg.NewMessage(fromUser, exampleSubject, exampleText, toUser)
 		m.SetHtml(exampleHtml)
-		options := TrackingOptions{
+		options := mailgun.TrackingOptions{
 			Tracking:       true,
 			TrackingClicks: "htmlonly",
 			TrackingOpens:  true,
@@ -174,13 +187,13 @@ func TestSendMGTrackingClicksHtmlOnly(t *testing.T) {
 }
 
 func TestSendMGTag(t *testing.T) {
-	if reason := SkipNetworkTest(); reason != "" {
+	if reason := mailgun.SkipNetworkTest(); reason != "" {
 		t.Skip(reason)
 	}
 
 	spendMoney(t, func() {
 		toUser := os.Getenv("MG_EMAIL_TO")
-		mg, err := NewMailgunFromEnv()
+		mg, err := mailgun.NewMailgunFromEnv()
 		ensure.Nil(t, err)
 
 		ctx := context.Background()
@@ -195,13 +208,13 @@ func TestSendMGTag(t *testing.T) {
 }
 
 func TestSendMGMIME(t *testing.T) {
-	if reason := SkipNetworkTest(); reason != "" {
+	if reason := mailgun.SkipNetworkTest(); reason != "" {
 		t.Skip(reason)
 	}
 
 	spendMoney(t, func() {
 		toUser := os.Getenv("MG_EMAIL_TO")
-		mg, err := NewMailgunFromEnv()
+		mg, err := mailgun.NewMailgunFromEnv()
 		ensure.Nil(t, err)
 
 		ctx := context.Background()
@@ -213,17 +226,17 @@ func TestSendMGMIME(t *testing.T) {
 }
 
 func TestSendMGBatchFailRecipients(t *testing.T) {
-	if reason := SkipNetworkTest(); reason != "" {
+	if reason := mailgun.SkipNetworkTest(); reason != "" {
 		t.Skip(reason)
 	}
 
 	spendMoney(t, func() {
 		toUser := os.Getenv("MG_EMAIL_TO")
-		mg, err := NewMailgunFromEnv()
+		mg, err := mailgun.NewMailgunFromEnv()
 		ensure.Nil(t, err)
 
 		m := mg.NewMessage(fromUser, exampleSubject, exampleText+"Batch\n")
-		for i := 0; i < MaxNumberOfRecipients; i++ {
+		for i := 0; i < mailgun.MaxNumberOfRecipients; i++ {
 			m.AddRecipient("") // We expect this to indicate a failure at the API
 		}
 		err = m.AddRecipientAndVariables(toUser, nil)
@@ -234,13 +247,13 @@ func TestSendMGBatchFailRecipients(t *testing.T) {
 }
 
 func TestSendMGBatchRecipientVariables(t *testing.T) {
-	if reason := SkipNetworkTest(); reason != "" {
+	if reason := mailgun.SkipNetworkTest(); reason != "" {
 		t.Skip(reason)
 	}
 
 	spendMoney(t, func() {
 		toUser := os.Getenv("MG_EMAIL_TO")
-		mg, err := NewMailgunFromEnv()
+		mg, err := mailgun.NewMailgunFromEnv()
 		ensure.Nil(t, err)
 
 		ctx := context.Background()
@@ -275,7 +288,7 @@ func TestSendMGOffline(t *testing.T) {
 	}))
 	defer srv.Close()
 
-	mg := NewMailgun(exampleDomain, exampleAPIKey)
+	mg := mailgun.NewMailgun(exampleDomain, exampleAPIKey)
 	mg.SetAPIBase(srv.URL + "/v3")
 	ctx := context.Background()
 
@@ -308,7 +321,7 @@ func TestSendMGSeparateDomain(t *testing.T) {
 	}))
 	defer srv.Close()
 
-	mg := NewMailgun(exampleDomain, exampleAPIKey)
+	mg := mailgun.NewMailgun(exampleDomain, exampleAPIKey)
 	mg.SetAPIBase(srv.URL + "/v3")
 
 	ctx := context.Background()
@@ -362,7 +375,7 @@ func TestSendMGMessageVariables(t *testing.T) {
 	}))
 	defer srv.Close()
 
-	mg := NewMailgun(exampleDomain, exampleAPIKey)
+	mg := mailgun.NewMailgun(exampleDomain, exampleAPIKey)
 	mg.SetAPIBase(srv.URL + "/v3")
 
 	m := mg.NewMessage(fromUser, exampleSubject, exampleText, toUser)
@@ -379,7 +392,7 @@ func TestSendMGMessageVariables(t *testing.T) {
 
 func TestAddRecipientsError(t *testing.T) {
 
-	mg := NewMailgun(exampleDomain, exampleAPIKey)
+	mg := mailgun.NewMailgun(exampleDomain, exampleAPIKey)
 	m := mg.NewMessage(fromUser, exampleSubject, exampleText)
 
 	for i := 0; i < 1000; i++ {
@@ -395,7 +408,7 @@ func TestAddRecipientsError(t *testing.T) {
 func TestAddRecipientAndVariablesError(t *testing.T) {
 	var err error
 
-	mg := NewMailgun(exampleDomain, exampleAPIKey)
+	mg := mailgun.NewMailgun(exampleDomain, exampleAPIKey)
 	m := mg.NewMessage(fromUser, exampleSubject, exampleText)
 
 	for i := 0; i < 1000; i++ {
@@ -433,7 +446,7 @@ func TestSendDomainError(t *testing.T) {
 
 	for _, c := range cases {
 		ctx := context.Background()
-		mg := NewMailgun(c.domain, exampleAPIKey)
+		mg := mailgun.NewMailgun(c.domain, exampleAPIKey)
 		mg.SetAPIBase(srv.URL + "/v3")
 		m := mg.NewMessage(fromUser, exampleSubject, exampleText, "test@test.com")
 
@@ -459,7 +472,7 @@ func TestSendEOFError(t *testing.T) {
 	}))
 	defer srv.Close()
 
-	mg := NewMailgun(exampleDomain, exampleAPIKey)
+	mg := mailgun.NewMailgun(exampleDomain, exampleAPIKey)
 	mg.SetAPIBase(srv.URL + "/v3")
 
 	m := mg.NewMessage(fromUser, exampleSubject, exampleText, toUser)
@@ -483,7 +496,7 @@ func TestHasRecipient(t *testing.T) {
 	}))
 	defer srv.Close()
 
-	mg := NewMailgun(exampleDomain, exampleAPIKey)
+	mg := mailgun.NewMailgun(exampleDomain, exampleAPIKey)
 	mg.SetAPIBase(srv.URL + "/v3")
 
 	// No recipient
@@ -523,7 +536,7 @@ func TestResendStored(t *testing.T) {
 	}))
 	defer srv.Close()
 
-	mg := NewMailgun(exampleDomain, exampleAPIKey)
+	mg := mailgun.NewMailgun(exampleDomain, exampleAPIKey)
 	mg.SetAPIBase(srv.URL + "/v3")
 
 	msg, id, err := mg.ReSend(context.Background(), srv.URL+"/v3/some-url")
@@ -555,7 +568,7 @@ func TestAddOverrideHeader(t *testing.T) {
 	}))
 	defer srv.Close()
 
-	mg := NewMailgun(exampleDomain, exampleAPIKey)
+	mg := mailgun.NewMailgun(exampleDomain, exampleAPIKey)
 	mg.SetAPIBase(srv.URL + "/v3")
 	mg.AddOverrideHeader("Host", "example.com")
 	mg.AddOverrideHeader("CustomHeader", "custom-value")
@@ -571,6 +584,36 @@ func TestAddOverrideHeader(t *testing.T) {
 	ensure.DeepEqual(t, id, exampleID)
 }
 
+func TestCaptureCurlOutput(t *testing.T) {
+	const (
+		exampleDomain  = "testDomain"
+		exampleAPIKey  = "testAPIKey"
+		toUser         = "test@test.com"
+		exampleMessage = "Queue. Thank you"
+		exampleID      = "<20111114174239.25659.5817@samples.mailgun.org>"
+	)
+	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+		ensure.DeepEqual(t, req.Method, http.MethodPost)
+		ensure.DeepEqual(t, req.URL.Path, fmt.Sprintf("/v3/%s/messages", exampleDomain))
+		rsp := fmt.Sprintf(`{"message":"%s", "id":"%s"}`, exampleMessage, exampleID)
+		fmt.Fprint(w, rsp)
+	}))
+	defer srv.Close()
+
+	mg := mailgun.NewMailgun(exampleDomain, exampleAPIKey)
+	mg.SetAPIBase(srv.URL + "/v3")
+	ctx := context.Background()
+
+	m := mg.NewMessage(fromUser, exampleSubject, exampleText, toUser)
+	msg, id, err := mg.Send(ctx, m)
+	ensure.Nil(t, err)
+	ensure.DeepEqual(t, msg, exampleMessage)
+	ensure.DeepEqual(t, id, exampleID)
+
+	ensure.StringContains(t, mg.GetCurlOutput(), "curl")
+	t.Logf("%s", mg.GetCurlOutput())
+}
+
 func TestSendTLSOptions(t *testing.T) {
 	const (
 		exampleDomain  = "testDomain"
@@ -593,7 +636,7 @@ func TestSendTLSOptions(t *testing.T) {
 	}))
 	defer srv.Close()
 
-	mg := NewMailgun(exampleDomain, exampleAPIKey)
+	mg := mailgun.NewMailgun(exampleDomain, exampleAPIKey)
 	mg.SetAPIBase(srv.URL + "/v3")
 	ctx := context.Background()
 
@@ -623,7 +666,7 @@ func TestSendTemplate(t *testing.T) {
 	}))
 	defer srv.Close()
 
-	mg := NewMailgun(exampleDomain, exampleAPIKey)
+	mg := mailgun.NewMailgun(exampleDomain, exampleAPIKey)
 	mg.SetAPIBase(srv.URL + "/v3")
 	ctx := context.Background()
 
@@ -656,7 +699,7 @@ func TestSendTemplateOptions(t *testing.T) {
 	}))
 	defer srv.Close()
 
-	mg := NewMailgun(exampleDomain, exampleAPIKey)
+	mg := mailgun.NewMailgun(exampleDomain, exampleAPIKey)
 	mg.SetAPIBase(srv.URL + "/v3")
 	ctx := context.Background()