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

DE-1203 subaccounts & X-Mailgun-On-Behalf-Of http header #318

Merged
merged 2 commits into from
Jan 24, 2024
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
4 changes: 2 additions & 2 deletions httphelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"github.com/pkg/errors"
)

var validURL = regexp.MustCompile(`/v[2-4].*`)
var validURL = regexp.MustCompile(`/v[2-5].*`)

type httpRequest struct {
URL string
Expand Down Expand Up @@ -364,7 +364,7 @@ func (r *httpRequest) curlString(req *http.Request, p payload) string {
parts = append(parts, fmt.Sprintf("-H \"Host: %s\"", req.Host))
}

//parts = append(parts, fmt.Sprintf(" --user '%s:%s'", r.BasicAuthUser, r.BasicAuthPassword))
// parts = append(parts, fmt.Sprintf(" --user '%s:%s'", r.BasicAuthUser, r.BasicAuthPassword))

if p != nil {
if p.getContentType() == "application/json" {
Expand Down
27 changes: 27 additions & 0 deletions mailgun.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ const (
listsEndpoint = "lists"
basicAuthUser = "api"
templatesEndpoint = "templates"
accountsEndpoint = "accounts"
subaccountsEndpoint = "subaccounts"
OnBehalfOfHeader = "X-Mailgun-On-Behalf-Of"
)

// Mailgun defines the supported subset of the Mailgun API.
Expand Down Expand Up @@ -246,6 +249,15 @@ type Mailgun interface {
UpdateTemplateVersion(ctx context.Context, templateName string, version *TemplateVersion) error
DeleteTemplateVersion(ctx context.Context, templateName, tag string) error
ListTemplateVersions(templateName string, opts *ListOptions) *TemplateVersionsIterator

ListSubaccounts(opts *ListSubaccountsOptions) *SubaccountsIterator
CreateSubaccount(ctx context.Context, subaccountName string) (SubaccountResponse, error)
SubaccountDetails(ctx context.Context, subaccountId string) (SubaccountResponse, error)
EnableSubaccount(ctx context.Context, subaccountId string) (SubaccountResponse, error)
DisableSubaccount(ctx context.Context, subaccountId string) (SubaccountResponse, error)

SetOnBehalfOfSubaccount(subaccountId string)
RemoveOnBehalfOfSubaccount()
}

// MailgunImpl bundles data needed by a large number of methods in order to interact with the Mailgun API.
Expand Down Expand Up @@ -317,6 +329,17 @@ func (mg *MailgunImpl) SetClient(c *http.Client) {
mg.client = c
}

// SetOnBehalfOfSubaccount sets X-Mailgun-On-Behalf-Of header to SUBACCOUNT_ACCOUNT_ID in order to perform API request
// on behalf of subaccount.
func (mg *MailgunImpl) SetOnBehalfOfSubaccount(subaccountId string) {
mg.AddOverrideHeader(OnBehalfOfHeader, subaccountId)
}

// RemoveOnBehalfOfSubaccount remove X-Mailgun-On-Behalf-Of header for primary usage.
func (mg *MailgunImpl) RemoveOnBehalfOfSubaccount() {
delete(mg.overrideHeaders, OnBehalfOfHeader)
}

// SetAPIBase updates the API Base URL for this client.
// // For EU Customers
// mg.SetAPIBase(mailgun.APIBaseEU)
Expand Down Expand Up @@ -402,6 +425,10 @@ func generatePublicApiUrl(m Mailgun, endpoint string) string {
return fmt.Sprintf("%s/%s", m.APIBase(), endpoint)
}

func generateSubaccountsApiUrl(m Mailgun) string {
return fmt.Sprintf("%s/%s/%s", m.APIBase(), accountsEndpoint, subaccountsEndpoint)
}

// generateParameterizedUrl works as generateApiUrl, but supports query parameters.
func generateParameterizedUrl(m Mailgun, endpoint string, payload payload) (string, error) {
paramBuffer, err := payload.getPayloadBuffer()
Expand Down
39 changes: 39 additions & 0 deletions messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,45 @@ func TestAddOverrideHeader(t *testing.T) {
ensure.StringContains(t, mg.GetCurlOutput(), "Host:")
}

func TestOnBehalfOfSubaccount(t *testing.T) {
const (
exampleDomain = "testDomain"
exampleAPIKey = "testAPIKey"
toUser = "[email protected]"
exampleMessage = "Queue. Thank you"
exampleID = "<[email protected]>"
)
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))
ensure.DeepEqual(t, req.Header.Get("CustomHeader"), "custom-value")
ensure.DeepEqual(t, req.Host, "example.com")
ensure.DeepEqual(t, req.Header.Get(mailgun.OnBehalfOfHeader), "mailgun.subaccount")

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")
mg.AddOverrideHeader("Host", "example.com")
mg.AddOverrideHeader("CustomHeader", "custom-value")
mg.SetOnBehalfOfSubaccount("mailgun.subaccount")
ctx := context.Background()

m := mg.NewMessage(fromUser, exampleSubject, exampleText, toUser)
m.SetRequireTLS(true)
m.SetSkipVerification(true)

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(), "Host:")
}

func TestCaptureCurlOutput(t *testing.T) {
const (
exampleDomain = "testDomain"
Expand Down
9 changes: 9 additions & 0 deletions mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type MockServer interface {
Events() []Event
Webhooks() WebHooksListResponse
Templates() []Template
SubaccountList() []Subaccount
}

// A mailgun api mock suitable for testing
Expand All @@ -47,6 +48,7 @@ type mockServer struct {
credentials []Credential
stats []Stats
tags []Tag
subaccountList []Subaccount
webhooks WebHooksListResponse
mutex sync.Mutex
}
Expand Down Expand Up @@ -105,6 +107,12 @@ func (ms *mockServer) Unsubscribes() []Unsubscribe {
return ms.unsubscribes
}

func (ms *mockServer) SubaccountList() []Subaccount {
defer ms.mutex.Unlock()
ms.mutex.Lock()
return ms.subaccountList
}

// Create a new instance of the mailgun API mock server
func NewMockServer() MockServer {
ms := mockServer{}
Expand All @@ -129,6 +137,7 @@ func NewMockServer() MockServer {
ms.addCredentialsRoutes(r)
ms.addStatsRoutes(r)
ms.addTagsRoutes(r)
ms.addSubaccountRoutes(r)
})
ms.addValidationRoutes(r)

Expand Down
127 changes: 127 additions & 0 deletions mock_subaccounts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package mailgun

import (
"net/http"

"github.com/go-chi/chi/v5"
)

func (ms *mockServer) addSubaccountRoutes(r chi.Router) {
ms.subaccountList = append(ms.subaccountList, Subaccount{
Id: "enabled.subaccount",
Name: "mailgun.test",
Status: "enabled",
}, Subaccount{
Id: "disabled.subaccount",
Name: "mailgun.test",
Status: "disabled",
})

r.Get("/accounts/subaccounts", ms.listSubaccounts)
r.Post("/accounts/subaccounts", ms.createSubaccount)

r.Get("/accounts/subaccounts/{subaccountID}", ms.getSubaccount)
r.Post("/accounts/subaccounts/{subaccountID}/enable", ms.enableSubaccount)
r.Post("/accounts/subaccounts/{subaccountID}/disable", ms.disableSubaccount)
}

func (ms *mockServer) listSubaccounts(w http.ResponseWriter, r *http.Request) {
defer ms.mutex.Unlock()
ms.mutex.Lock()

var list subaccountsListResponse
for _, subaccount := range ms.subaccountList {
list.Items = append(list.Items, subaccount)
}

skip := stringToInt(r.FormValue("skip"))
limit := stringToInt(r.FormValue("limit"))
if limit == 0 {
limit = 100
}

if skip > len(list.Items) {
skip = len(list.Items)
}

end := limit + skip
if end > len(list.Items) {
end = len(list.Items)
}

// If we are at the end of the list
if skip == end {
toJSON(w, subaccountsListResponse{
Total: len(list.Items),
Items: []Subaccount{},
})
return
}

toJSON(w, subaccountsListResponse{
Total: len(list.Items),
Items: list.Items[skip:end],
})
}

func (ms *mockServer) getSubaccount(w http.ResponseWriter, r *http.Request) {
defer ms.mutex.Unlock()
ms.mutex.Lock()

for _, s := range ms.subaccountList {
if s.Id == chi.URLParam(r, "subaccountID") {
toJSON(w, SubaccountResponse{Item: s})
return
}
}
w.WriteHeader(http.StatusNotFound)
toJSON(w, okResp{Message: "Not Found"})
}

func (ms *mockServer) createSubaccount(w http.ResponseWriter, r *http.Request) {
defer ms.mutex.Unlock()
ms.mutex.Lock()

ms.subaccountList = append(ms.subaccountList, Subaccount{
Id: "test",
Name: r.FormValue("name"),
Status: "active",
})
toJSON(w, okResp{Message: "Subaccount has been created"})
}

func (ms *mockServer) enableSubaccount(w http.ResponseWriter, r *http.Request) {
defer ms.mutex.Unlock()
ms.mutex.Lock()

for _, subaccount := range ms.subaccountList {
if subaccount.Id == chi.URLParam(r, "subaccountID") && subaccount.Status == "disabled" {
subaccount.Status = "enabled"
toJSON(w, SubaccountResponse{Item: subaccount})
return
}
if subaccount.Id == chi.URLParam(r, "subaccountID") && subaccount.Status == "enabled" {
toJSON(w, okResp{Message: "subaccount is already enabled"})
return
}
}
toJSON(w, okResp{Message: "Not Found"})
}

func (ms *mockServer) disableSubaccount(w http.ResponseWriter, r *http.Request) {
defer ms.mutex.Unlock()
ms.mutex.Lock()

for _, subaccount := range ms.subaccountList {
if subaccount.Id == chi.URLParam(r, "subaccountID") && subaccount.Status == "enabled" {
subaccount.Status = "disabled"
toJSON(w, SubaccountResponse{Item: subaccount})
return
}
if subaccount.Id == chi.URLParam(r, "subaccountID") && subaccount.Status == "disabled" {
toJSON(w, okResp{Message: "subaccount is already disabled"})
return
}
}
toJSON(w, okResp{Message: "Not Found"})
}
Loading
Loading