Skip to content
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Account & organization management:
- Permissions — list resources & bulk-update access permissions — [`examples/permissions`](examples/permissions)
- API token management — list, create, get, reset & delete — [`examples/api-tokens`](examples/api-tokens)
- Billing — current billing-cycle usage across Sandbox, Sending & Marketing — [`examples/billing`](examples/billing)
- Organization sub-accounts — list & create — [`examples/sub-accounts`](examples/sub-accounts)

Contact management:

Expand Down
19 changes: 19 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ type Client struct {
bulk bool
sandboxID int64

// organizationID scopes SubAccounts operations; set via WithOrganizationID.
organizationID int64

// Projects manages sandbox projects.
Projects *ProjectsService
// Sandboxes manages sandboxes (testing inboxes) and their actions.
Expand Down Expand Up @@ -89,6 +92,8 @@ type Client struct {
APITokens *APITokensService
// Billing reads current billing-cycle usage.
Billing *BillingService
// SubAccounts lists and creates sub-accounts within an organization.
SubAccounts *SubAccountsService

// Contacts manages email marketing contacts.
Contacts *ContactsService
Expand Down Expand Up @@ -161,6 +166,7 @@ func NewClient(token string, opts ...Option) (*Client, error) {
c.Permissions = &PermissionsService{client: c}
c.APITokens = &APITokensService{client: c}
c.Billing = &BillingService{client: c}
c.SubAccounts = &SubAccountsService{client: c}
c.Contacts = &ContactsService{client: c}
c.ContactEvents = &ContactEventsService{client: c}
c.ContactLists = &ContactListsService{client: c}
Expand Down Expand Up @@ -247,6 +253,19 @@ func WithSandboxID(sandboxID int64) Option {
}
}

// WithOrganizationID sets the organization that SubAccounts operations target.
// A token can access a single organization, so it is configured once here
// rather than passed to every call. Required before using client.SubAccounts.
func WithOrganizationID(organizationID int64) Option {
return func(c *Client) error {
if organizationID <= 0 {
return fmt.Errorf("mailtrap: organization ID must be valid, got %d", organizationID)
}
c.organizationID = organizationID
return nil
}
}

// WithBaseURL overrides the base URL for a host, primarily for testing against
// an httptest server.
func WithBaseURL(host Host, rawURL string) Option {
Expand Down
2 changes: 2 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ func TestNewClient_validation(t *testing.T) {
{name: "bulk and sandbox conflict", token: "tok", opts: []mailtrap.Option{mailtrap.WithSandbox(true), mailtrap.WithSandboxID(1), mailtrap.WithBulk(true)}, wantErr: true},
{name: "stray sandbox id ignored without sandbox", token: "tok", opts: []mailtrap.Option{mailtrap.WithSandboxID(1)}},
{name: "negative sandbox id", token: "tok", opts: []mailtrap.Option{mailtrap.WithSandboxID(-1)}, wantErr: true},
{name: "valid organization id", token: "tok", opts: []mailtrap.Option{mailtrap.WithOrganizationID(1001)}},
{name: "invalid organization id", token: "tok", opts: []mailtrap.Option{mailtrap.WithOrganizationID(0)}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
39 changes: 39 additions & 0 deletions examples/sub-accounts/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package main

import (
"context"
"fmt"
"log"
"os"
"strconv"

"github.com/mailtrap/mailtrap-go"
)

func main() {
organizationID, err := strconv.ParseInt(os.Getenv("MAILTRAP_ORGANIZATION_ID"), 10, 64)
if err != nil {
log.Fatal(err)
}

client, err := mailtrap.NewClient(os.Getenv("MAILTRAP_API_TOKEN"),
mailtrap.WithOrganizationID(organizationID),
)
if err != nil {
log.Fatal(err)
}

ctx := context.Background()

subAccount, _, err := client.SubAccounts.Create(ctx, "Development Team Account")
if err != nil {
log.Fatal(err)
}
fmt.Printf("created sub-account %d (%s)\n", subAccount.ID, subAccount.Name)

subAccounts, _, err := client.SubAccounts.List(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("organization has %d sub-account(s)\n", len(subAccounts))
}
49 changes: 49 additions & 0 deletions sub_accounts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package mailtrap

import (
"context"
"errors"
"fmt"
"net/http"
)

// errNoOrganizationID is returned by SubAccounts operations when the client was
// created without WithOrganizationID.
var errNoOrganizationID = errors.New("mailtrap: WithOrganizationID is required for sub-account operations")

// SubAccountsService lists and creates the sub-accounts of an organization.
// These endpoints require an organization token with sub-account management
// permission.
type SubAccountsService struct {
client *Client
}

// SubAccount is an account within an organization.
type SubAccount struct {
ID int64 `json:"id"`
Name string `json:"name"`
}

// List returns the sub-accounts of the organization set with WithOrganizationID.
func (s *SubAccountsService) List(ctx context.Context) ([]*SubAccount, *Response, error) {
if s.client.organizationID == 0 {
return nil, nil, errNoOrganizationID
}
path := fmt.Sprintf("/api/organizations/%d/sub_accounts", s.client.organizationID)
var subAccounts []*SubAccount
resp, err := s.client.do(ctx, HostGeneral, http.MethodGet, path, nil, nil, &subAccounts)
return subAccounts, resp, err
}

// Create adds a sub-account with the given name under the organization set with
// WithOrganizationID.
func (s *SubAccountsService) Create(ctx context.Context, name string) (*SubAccount, *Response, error) {
if s.client.organizationID == 0 {
return nil, nil, errNoOrganizationID
}
path := fmt.Sprintf("/api/organizations/%d/sub_accounts", s.client.organizationID)
body := map[string]any{"account": map[string]string{"name": name}}
subAccount := new(SubAccount)
resp, err := s.client.do(ctx, HostGeneral, http.MethodPost, path, nil, body, subAccount)
return subAccount, resp, err
}
50 changes: 50 additions & 0 deletions sub_accounts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package mailtrap_test

import (
"context"
"net/http"
"testing"

"github.com/mailtrap/mailtrap-go"
)

func TestSubAccounts_List(t *testing.T) {
mux, client := setup(t, mailtrap.WithOrganizationID(1001))
mux.HandleFunc("GET /api/organizations/1001/sub_accounts", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`[{"id":12345,"name":"Development Team Account"},{"id":12346,"name":"QA Team Account"}]`))
})

subAccounts, _, err := client.SubAccounts.List(context.Background())
if err != nil {
t.Fatalf("List: %v", err)
}
if len(subAccounts) != 2 || subAccounts[0].ID != 12345 || subAccounts[0].Name != "Development Team Account" {
t.Fatalf("subAccounts = %+v", subAccounts)
}
}

func TestSubAccounts_Create(t *testing.T) {
mux, client := setup(t, mailtrap.WithOrganizationID(1001))
mux.HandleFunc("POST /api/organizations/1001/sub_accounts", func(w http.ResponseWriter, r *http.Request) {
wantJSONBody(t, r, `{"account":{"name":"New Team Account"}}`)
_, _ = w.Write([]byte(`{"id":12347,"name":"New Team Account"}`))
})

subAccount, _, err := client.SubAccounts.Create(context.Background(), "New Team Account")
if err != nil {
t.Fatalf("Create: %v", err)
}
if subAccount.ID != 12347 || subAccount.Name != "New Team Account" {
t.Errorf("subAccount = %+v", subAccount)
}
}

func TestSubAccounts_requiresOrganizationID(t *testing.T) {
_, client := setup(t) // no WithOrganizationID
if _, _, err := client.SubAccounts.List(context.Background()); err == nil {
t.Error("List without organization ID: want error, got nil")
}
if _, _, err := client.SubAccounts.Create(context.Background(), "x"); err == nil {
t.Error("Create without organization ID: want error, got nil")
}
}