From 9f2e68d1a4d128bc7e48c350f08f0eaa04beb57c Mon Sep 17 00:00:00 2001 From: Marcin Klocek Date: Tue, 25 Aug 2026 16:44:12 +0200 Subject: [PATCH] Add sending domain update and company info commands Fills the gap against the OpenAPI spec: PATCH on a domain plus the nested company_info GET/POST/PATCH. company-info is its own command group, following emailcampaigns: the spec path is /api/domains/{id}/company_info and the account comes from the API token, so these commands do not require an account id. Both update commands only send flags the user actually passed, via cmd.Flags().Changed, so a partial update cannot clear unrelated fields, and both reject an update with no attribute flags rather than sending an empty wrapper, following emailcampaigns update. domains update renders an extended column set so all five settings it can write are visible in its output. --- README.md | 10 +- cmd/root.go | 18 +- internal/commands/companyinfo/companyinfo.go | 54 ++++ .../commands/companyinfo/companyinfo_test.go | 268 ++++++++++++++++++ internal/commands/companyinfo/create.go | 100 +++++++ internal/commands/companyinfo/get.go | 41 +++ internal/commands/companyinfo/update.go | 92 ++++++ internal/commands/domains/domains.go | 1 + .../commands/domains/domains_extended_test.go | 87 ++++++ internal/commands/domains/list.go | 24 +- internal/commands/domains/update.go | 84 ++++++ 11 files changed, 764 insertions(+), 15 deletions(-) create mode 100644 internal/commands/companyinfo/companyinfo.go create mode 100644 internal/commands/companyinfo/companyinfo_test.go create mode 100644 internal/commands/companyinfo/create.go create mode 100644 internal/commands/companyinfo/get.go create mode 100644 internal/commands/companyinfo/update.go create mode 100644 internal/commands/domains/update.go diff --git a/README.md b/README.md index ef82114..3901f88 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,15 @@ mailtrap messages spam-score --sandbox-id 12345 --id 67890 # Domains mailtrap domains list mailtrap domains create --name "yourdomain.com" +mailtrap domains update --id 123 --open-tracking --click-tracking --tracking-opt-out mailtrap domains send-setup-instructions --id 123 --email "admin@yourdomain.com" +# Company info (required for domain compliance verification) +mailtrap company-info get --domain-id 123 +mailtrap company-info create --domain-id 123 --name "Your Company" --address "123 Main St" \ + --city "San Francisco" --country US --zip-code 94105 --website-url "https://yourdomain.com" +mailtrap company-info update --domain-id 123 --city "New York" --zip-code 10001 + # Templates mailtrap templates list mailtrap templates create --name "Welcome" --subject "Hello {{name}}" --body-html '

Hi!

' @@ -161,7 +168,8 @@ mailtrap domains list --output text |-------|----------| | **Sending** | `send transactional`, `send bulk`, `send batch-transactional`, `send batch-bulk` | | **Sandbox Send** | `sandbox-send single`, `sandbox-send batch` | -| **Domains** | `domains list`, `domains get`, `domains create`, `domains delete`, `domains send-setup-instructions` | +| **Domains** | `domains list`, `domains get`, `domains create`, `domains update`, `domains delete`, `domains send-setup-instructions` | +| **Company Info** | `company-info get`, `company-info create`, `company-info update` | | **Templates** | `templates list`, `templates get`, `templates create`, `templates update`, `templates delete` | | **Suppressions** | `suppressions list`, `suppressions delete` | | **Webhooks** | `webhooks list`, `webhooks get`, `webhooks create`, `webhooks update`, `webhooks delete` | diff --git a/cmd/root.go b/cmd/root.go index c572e86..6194d28 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -11,20 +11,21 @@ import ( "github.com/mailtrap/mailtrap-cli/internal/commands/accounts" "github.com/mailtrap/mailtrap-cli/internal/commands/attachments" "github.com/mailtrap/mailtrap-cli/internal/commands/billing" + "github.com/mailtrap/mailtrap-cli/internal/commands/companyinfo" + "github.com/mailtrap/mailtrap-cli/internal/commands/configure" "github.com/mailtrap/mailtrap-cli/internal/commands/contact_fields" "github.com/mailtrap/mailtrap-cli/internal/commands/contact_lists" - "github.com/mailtrap/mailtrap-cli/internal/commands/configure" "github.com/mailtrap/mailtrap-cli/internal/commands/contacts" "github.com/mailtrap/mailtrap-cli/internal/commands/domains" email_logs "github.com/mailtrap/mailtrap-cli/internal/commands/email_logs" "github.com/mailtrap/mailtrap-cli/internal/commands/emailcampaigns" "github.com/mailtrap/mailtrap-cli/internal/commands/inbound" - "github.com/mailtrap/mailtrap-cli/internal/commands/sandboxes" "github.com/mailtrap/mailtrap-cli/internal/commands/messages" "github.com/mailtrap/mailtrap-cli/internal/commands/organizations" "github.com/mailtrap/mailtrap-cli/internal/commands/permissions" "github.com/mailtrap/mailtrap-cli/internal/commands/projects" "github.com/mailtrap/mailtrap-cli/internal/commands/sandbox_send" + "github.com/mailtrap/mailtrap-cli/internal/commands/sandboxes" "github.com/mailtrap/mailtrap-cli/internal/commands/send" "github.com/mailtrap/mailtrap-cli/internal/commands/stats" "github.com/mailtrap/mailtrap-cli/internal/commands/suppressions" @@ -35,9 +36,9 @@ import ( func NewRootCmd(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ - Use: "mailtrap", - Short: "CLI for the Mailtrap email platform", - Long: "A command-line interface for managing Mailtrap email sending, sandbox testing, contacts, and account settings.", + Use: "mailtrap", + Short: "CLI for the Mailtrap email platform", + Long: "A command-line interface for managing Mailtrap email sending, sandbox testing, contacts, and account settings.", SilenceUsage: true, SilenceErrors: true, } @@ -56,6 +57,7 @@ func NewRootCmd(f *cmdutil.Factory) *cobra.Command { // Sending Management cmd.AddCommand(domains.NewCmdDomains(f)) + cmd.AddCommand(companyinfo.NewCmdCompanyInfo(f)) cmd.AddCommand(suppressions.NewCmdSuppressions(f)) cmd.AddCommand(stats.NewCmdStats(f)) cmd.AddCommand(templates.NewCmdTemplates(f)) @@ -96,9 +98,9 @@ func NewRootCmd(f *cmdutil.Factory) *cobra.Command { func newCompletionCmd() *cobra.Command { return &cobra.Command{ - Use: "completion [bash|zsh|fish|powershell]", - Short: "Generate shell completion scripts", - Args: cobra.ExactArgs(1), + Use: "completion [bash|zsh|fish|powershell]", + Short: "Generate shell completion scripts", + Args: cobra.ExactArgs(1), ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, RunE: func(cmd *cobra.Command, args []string) error { switch args[0] { diff --git a/internal/commands/companyinfo/companyinfo.go b/internal/commands/companyinfo/companyinfo.go new file mode 100644 index 0000000..8c662cf --- /dev/null +++ b/internal/commands/companyinfo/companyinfo.go @@ -0,0 +1,54 @@ +package companyinfo + +import ( + "github.com/mailtrap/mailtrap-cli/internal/cmdutil" + "github.com/mailtrap/mailtrap-cli/internal/output" + "github.com/spf13/cobra" +) + +type CompanyInfo struct { + Name *string `json:"name,omitempty"` + Address *string `json:"address,omitempty"` + City *string `json:"city,omitempty"` + Country *string `json:"country,omitempty"` + Phone *string `json:"phone,omitempty"` + ZipCode *string `json:"zip_code,omitempty"` + PrivacyPolicyURL *string `json:"privacy_policy_url,omitempty"` + TermsOfServiceURL *string `json:"terms_of_service_url,omitempty"` + WebsiteURL *string `json:"website_url,omitempty"` + InfoLevel *string `json:"info_level,omitempty"` +} + +type companyInfoResponse struct { + Data CompanyInfo `json:"data"` +} + +var companyInfoColumns = []output.Column{ + {Header: "NAME", Field: "name"}, + {Header: "ADDRESS", Field: "address"}, + {Header: "CITY", Field: "city"}, + {Header: "COUNTRY", Field: "country"}, + {Header: "ZIP CODE", Field: "zip_code"}, + {Header: "PHONE", Field: "phone"}, + {Header: "WEBSITE", Field: "website_url"}, + {Header: "PRIVACY POLICY", Field: "privacy_policy_url"}, + {Header: "TERMS OF SERVICE", Field: "terms_of_service_url"}, + {Header: "INFO LEVEL", Field: "info_level"}, +} + +func companyInfoPath(domainID string) string { + return "/api/domains/" + domainID + "/company_info" +} + +func NewCmdCompanyInfo(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "company-info", + Short: "Manage sending domain company info", + } + + cmd.AddCommand(NewCmdGet(f)) + cmd.AddCommand(NewCmdCreate(f)) + cmd.AddCommand(NewCmdUpdate(f)) + + return cmd +} diff --git a/internal/commands/companyinfo/companyinfo_test.go b/internal/commands/companyinfo/companyinfo_test.go new file mode 100644 index 0000000..40598ce --- /dev/null +++ b/internal/commands/companyinfo/companyinfo_test.go @@ -0,0 +1,268 @@ +package companyinfo_test + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/mailtrap/mailtrap-cli/internal/client" + "github.com/mailtrap/mailtrap-cli/internal/cmdutil" + "github.com/mailtrap/mailtrap-cli/internal/commands/companyinfo" + "github.com/mailtrap/mailtrap-cli/internal/config" + "github.com/spf13/viper" +) + +func setupTest(handler http.HandlerFunc) (*cmdutil.Factory, *bytes.Buffer, func()) { + server := httptest.NewServer(handler) + + c := client.New("test-token") + c.SetBaseURL(client.BaseGeneral, server.URL) + + buf := &bytes.Buffer{} + f := &cmdutil.Factory{ + Config: func() *config.Config { + return &config.Config{APIToken: "test-token", AccountID: "123"} + }, + IOStreams: &cmdutil.IOStreams{ + Out: buf, + ErrOut: &bytes.Buffer{}, + }, + ClientOverride: c, + } + + viper.Set("api-token", "test-token") + viper.Set("account-id", "123") + viper.Set("output", "table") + + return f, buf, func() { + server.Close() + viper.Reset() + } +} + +func companyInfoPayload() map[string]interface{} { + return map[string]interface{}{ + "data": map[string]interface{}{ + "name": "Mailtrap", + "address": "123 Main St", + "city": "San Francisco", + "country": "US", + "phone": "+1-555-0100", + "zip_code": "94105", + "website_url": "https://mailtrap.io", + "info_level": "business", + }, + } +} + +func TestCompanyInfoGet(t *testing.T) { + f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/api/domains/435/company_info" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + if r.Header.Get("Api-Token") != "test-token" { + t.Errorf("expected Api-Token header 'test-token', got %q", r.Header.Get("Api-Token")) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(companyInfoPayload()) + }) + defer cleanup() + + cmd := companyinfo.NewCmdCompanyInfo(f) + cmd.SetArgs([]string{"get", "--domain-id", "435"}) + cmd.SetOut(buf) + + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + output := buf.String() + if !strings.Contains(output, "Mailtrap") { + t.Errorf("expected output to contain 'Mailtrap', got:\n%s", output) + } + if !strings.Contains(output, "business") { + t.Errorf("expected output to contain 'business', got:\n%s", output) + } +} + +func TestCompanyInfoGetJSON(t *testing.T) { + f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(companyInfoPayload()) + }) + defer cleanup() + + viper.Set("output", "json") + + cmd := companyinfo.NewCmdCompanyInfo(f) + cmd.SetArgs([]string{"get", "--domain-id", "435"}) + cmd.SetOut(buf) + + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(buf.Bytes(), &result); err != nil { + t.Fatalf("failed to unmarshal output: %v", err) + } + if result["name"] != "Mailtrap" { + t.Errorf("expected name 'Mailtrap', got %v", result["name"]) + } + if result["zip_code"] != "94105" { + t.Errorf("expected zip_code '94105', got %v", result["zip_code"]) + } +} + +func TestCompanyInfoGetMissingDomainID(t *testing.T) { + f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) { + t.Error("expected no request to be sent") + }) + defer cleanup() + + cmd := companyinfo.NewCmdCompanyInfo(f) + cmd.SetArgs([]string{"get"}) + cmd.SetOut(buf) + + err := cmd.Execute() + if err == nil { + t.Fatal("expected error for missing --domain-id") + } + if !strings.Contains(err.Error(), "--domain-id is required") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestCompanyInfoCreate(t *testing.T) { + f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/api/domains/435/company_info" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + + body, _ := io.ReadAll(r.Body) + var reqBody map[string]map[string]interface{} + json.Unmarshal(body, &reqBody) + + fields := reqBody["company_info"] + if fields["name"] != "Mailtrap" { + t.Errorf("expected name 'Mailtrap', got %v", fields["name"]) + } + if fields["website_url"] != "https://mailtrap.io" { + t.Errorf("expected website_url 'https://mailtrap.io', got %v", fields["website_url"]) + } + if _, ok := fields["phone"]; ok { + t.Error("expected unset phone flag to be omitted from the body") + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(companyInfoPayload()) + }) + defer cleanup() + + cmd := companyinfo.NewCmdCompanyInfo(f) + cmd.SetArgs([]string{ + "create", "--domain-id", "435", + "--name", "Mailtrap", + "--address", "123 Main St", + "--city", "San Francisco", + "--country", "US", + "--zip-code", "94105", + "--website-url", "https://mailtrap.io", + }) + cmd.SetOut(buf) + + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !strings.Contains(buf.String(), "Mailtrap") { + t.Errorf("expected output to contain 'Mailtrap', got:\n%s", buf.String()) + } +} + +func TestCompanyInfoCreateMissingRequiredFlag(t *testing.T) { + f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) { + t.Error("expected no request to be sent") + }) + defer cleanup() + + cmd := companyinfo.NewCmdCompanyInfo(f) + cmd.SetArgs([]string{"create", "--domain-id", "435", "--name", "Mailtrap"}) + cmd.SetOut(buf) + + err := cmd.Execute() + if err == nil { + t.Fatal("expected error for missing --address") + } + if !strings.Contains(err.Error(), "--address is required") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestCompanyInfoUpdate(t *testing.T) { + f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPatch { + t.Errorf("expected PATCH, got %s", r.Method) + } + if r.URL.Path != "/api/domains/435/company_info" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + + body, _ := io.ReadAll(r.Body) + var reqBody map[string]map[string]interface{} + json.Unmarshal(body, &reqBody) + + fields := reqBody["company_info"] + if len(fields) != 2 { + t.Errorf("expected only the changed flags in the body, got %v", fields) + } + if fields["city"] != "New York" { + t.Errorf("expected city 'New York', got %v", fields["city"]) + } + if fields["zip_code"] != "10001" { + t.Errorf("expected zip_code '10001', got %v", fields["zip_code"]) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(companyInfoPayload()) + }) + defer cleanup() + + cmd := companyinfo.NewCmdCompanyInfo(f) + cmd.SetArgs([]string{"update", "--domain-id", "435", "--city", "New York", "--zip-code", "10001"}) + cmd.SetOut(buf) + + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestCompanyInfoUpdateNoFields(t *testing.T) { + f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) { + t.Error("expected no request to be sent") + }) + defer cleanup() + + cmd := companyinfo.NewCmdCompanyInfo(f) + cmd.SetArgs([]string{"update", "--domain-id", "435"}) + cmd.SetOut(buf) + + err := cmd.Execute() + if err == nil { + t.Fatal("expected error when no attribute flag is passed") + } + if !strings.Contains(err.Error(), "at least one attribute flag is required") { + t.Errorf("unexpected error: %v", err) + } +} diff --git a/internal/commands/companyinfo/create.go b/internal/commands/companyinfo/create.go new file mode 100644 index 0000000..2531b68 --- /dev/null +++ b/internal/commands/companyinfo/create.go @@ -0,0 +1,100 @@ +package companyinfo + +import ( + "context" + + "github.com/mailtrap/mailtrap-cli/internal/client" + "github.com/mailtrap/mailtrap-cli/internal/cmdutil" + "github.com/mailtrap/mailtrap-cli/internal/output" + "github.com/spf13/cobra" +) + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + domainID string + name string + address string + city string + country string + zipCode string + websiteURL string + phone string + privacyPolicyURL string + termsOfServiceURL string + infoLevel string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create company info for a sending domain", + RunE: func(cmd *cobra.Command, args []string) error { + required := []struct { + flag string + value string + }{ + {"domain-id", domainID}, + {"name", name}, + {"address", address}, + {"city", city}, + {"country", country}, + {"zip-code", zipCode}, + {"website-url", websiteURL}, + } + for _, r := range required { + if err := cmdutil.RequireFlag(r.flag, r.value); err != nil { + return err + } + } + + c, err := f.NewClient() + if err != nil { + return err + } + + companyInfoFields := map[string]interface{}{ + "name": name, + "address": address, + "city": city, + "country": country, + "zip_code": zipCode, + "website_url": websiteURL, + } + if cmd.Flags().Changed("phone") { + companyInfoFields["phone"] = phone + } + if cmd.Flags().Changed("privacy-policy-url") { + companyInfoFields["privacy_policy_url"] = privacyPolicyURL + } + if cmd.Flags().Changed("terms-of-service-url") { + companyInfoFields["terms_of_service_url"] = termsOfServiceURL + } + if cmd.Flags().Changed("info-level") { + companyInfoFields["info_level"] = infoLevel + } + + body := map[string]interface{}{"company_info": companyInfoFields} + + var resp companyInfoResponse + if err := c.Post(context.Background(), client.BaseGeneral, companyInfoPath(domainID), body, &resp); err != nil { + return err + } + + format := cmdutil.GetOutputFormat() + return output.Print(f.IOStreams.Out, format, resp.Data, companyInfoColumns) + }, + } + + cmd.Flags().StringVar(&domainID, "domain-id", "", "Sending domain ID (required)") + cmd.Flags().StringVar(&name, "name", "", "Company or individual name (required)") + cmd.Flags().StringVar(&address, "address", "", "Street address (required)") + cmd.Flags().StringVar(&city, "city", "", "City (required)") + cmd.Flags().StringVar(&country, "country", "", "Country (required)") + cmd.Flags().StringVar(&zipCode, "zip-code", "", "ZIP or postal code (required)") + cmd.Flags().StringVar(&websiteURL, "website-url", "", "Company website URL (required)") + cmd.Flags().StringVar(&phone, "phone", "", "Phone number") + cmd.Flags().StringVar(&privacyPolicyURL, "privacy-policy-url", "", "URL to the privacy policy page") + cmd.Flags().StringVar(&termsOfServiceURL, "terms-of-service-url", "", "URL to the terms of service page") + cmd.Flags().StringVar(&infoLevel, "info-level", "", "Whether the sender is a business or individual: business, individual") + + return cmd +} diff --git a/internal/commands/companyinfo/get.go b/internal/commands/companyinfo/get.go new file mode 100644 index 0000000..d595ef8 --- /dev/null +++ b/internal/commands/companyinfo/get.go @@ -0,0 +1,41 @@ +package companyinfo + +import ( + "context" + + "github.com/mailtrap/mailtrap-cli/internal/client" + "github.com/mailtrap/mailtrap-cli/internal/cmdutil" + "github.com/mailtrap/mailtrap-cli/internal/output" + "github.com/spf13/cobra" +) + +func NewCmdGet(f *cmdutil.Factory) *cobra.Command { + var domainID string + + cmd := &cobra.Command{ + Use: "get", + Short: "Get company info for a sending domain", + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.RequireFlag("domain-id", domainID); err != nil { + return err + } + + c, err := f.NewClient() + if err != nil { + return err + } + + var resp companyInfoResponse + if err := c.Get(context.Background(), client.BaseGeneral, companyInfoPath(domainID), nil, &resp); err != nil { + return err + } + + format := cmdutil.GetOutputFormat() + return output.Print(f.IOStreams.Out, format, resp.Data, companyInfoColumns) + }, + } + + cmd.Flags().StringVar(&domainID, "domain-id", "", "Sending domain ID (required)") + + return cmd +} diff --git a/internal/commands/companyinfo/update.go b/internal/commands/companyinfo/update.go new file mode 100644 index 0000000..1722c2a --- /dev/null +++ b/internal/commands/companyinfo/update.go @@ -0,0 +1,92 @@ +package companyinfo + +import ( + "context" + "fmt" + + "github.com/mailtrap/mailtrap-cli/internal/client" + "github.com/mailtrap/mailtrap-cli/internal/cmdutil" + "github.com/mailtrap/mailtrap-cli/internal/output" + "github.com/spf13/cobra" +) + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + domainID string + name string + address string + city string + country string + zipCode string + websiteURL string + phone string + privacyPolicyURL string + termsOfServiceURL string + infoLevel string + ) + + cmd := &cobra.Command{ + Use: "update", + Short: "Update company info for a sending domain", + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.RequireFlag("domain-id", domainID); err != nil { + return err + } + + c, err := f.NewClient() + if err != nil { + return err + } + + companyInfoFields := map[string]interface{}{} + for _, field := range []struct { + flag string + key string + value string + }{ + {"name", "name", name}, + {"address", "address", address}, + {"city", "city", city}, + {"country", "country", country}, + {"zip-code", "zip_code", zipCode}, + {"website-url", "website_url", websiteURL}, + {"phone", "phone", phone}, + {"privacy-policy-url", "privacy_policy_url", privacyPolicyURL}, + {"terms-of-service-url", "terms_of_service_url", termsOfServiceURL}, + {"info-level", "info_level", infoLevel}, + } { + if cmd.Flags().Changed(field.flag) { + companyInfoFields[field.key] = field.value + } + } + + if len(companyInfoFields) == 0 { + return fmt.Errorf("at least one attribute flag is required") + } + + body := map[string]interface{}{"company_info": companyInfoFields} + + var resp companyInfoResponse + if err := c.Patch(context.Background(), client.BaseGeneral, companyInfoPath(domainID), body, &resp); err != nil { + return err + } + + format := cmdutil.GetOutputFormat() + return output.Print(f.IOStreams.Out, format, resp.Data, companyInfoColumns) + }, + } + + cmd.Flags().StringVar(&domainID, "domain-id", "", "Sending domain ID (required)") + cmd.Flags().StringVar(&name, "name", "", "Company or individual name") + cmd.Flags().StringVar(&address, "address", "", "Street address") + cmd.Flags().StringVar(&city, "city", "", "City") + cmd.Flags().StringVar(&country, "country", "", "Country") + cmd.Flags().StringVar(&zipCode, "zip-code", "", "ZIP or postal code") + cmd.Flags().StringVar(&websiteURL, "website-url", "", "Company website URL") + cmd.Flags().StringVar(&phone, "phone", "", "Phone number") + cmd.Flags().StringVar(&privacyPolicyURL, "privacy-policy-url", "", "URL to the privacy policy page") + cmd.Flags().StringVar(&termsOfServiceURL, "terms-of-service-url", "", "URL to the terms of service page") + cmd.Flags().StringVar(&infoLevel, "info-level", "", "Whether the sender is a business or individual: business, individual") + + return cmd +} diff --git a/internal/commands/domains/domains.go b/internal/commands/domains/domains.go index ebd81a5..120640d 100644 --- a/internal/commands/domains/domains.go +++ b/internal/commands/domains/domains.go @@ -14,6 +14,7 @@ func NewCmdDomains(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(NewCmdList(f)) cmd.AddCommand(NewCmdGet(f)) cmd.AddCommand(NewCmdCreate(f)) + cmd.AddCommand(NewCmdUpdate(f)) cmd.AddCommand(NewCmdDelete(f)) cmd.AddCommand(NewCmdSendSetupInstructions(f)) diff --git a/internal/commands/domains/domains_extended_test.go b/internal/commands/domains/domains_extended_test.go index a238e43..a1d6e96 100644 --- a/internal/commands/domains/domains_extended_test.go +++ b/internal/commands/domains/domains_extended_test.go @@ -66,3 +66,90 @@ func TestDomainsSendSetupInstructionsMissingFlags(t *testing.T) { t.Errorf("expected '--id is required' error, got: %v", err) } } + +func TestDomainsUpdate(t *testing.T) { + f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPatch { + t.Errorf("expected PATCH, got %s", r.Method) + } + if !strings.HasSuffix(r.URL.Path, "/api/accounts/123/sending_domains/1") { + t.Errorf("unexpected path: %s", r.URL.Path) + } + + body, _ := io.ReadAll(r.Body) + var reqBody map[string]map[string]interface{} + json.Unmarshal(body, &reqBody) + + fields := reqBody["sending_domain"] + if len(fields) != 2 { + t.Errorf("expected only the changed flags in the body, got %v", fields) + } + if fields["tracking_opt_out_enabled"] != true { + t.Errorf("expected tracking_opt_out_enabled true, got %v", fields["tracking_opt_out_enabled"]) + } + if fields["auto_unsubscribe_link_enabled"] != false { + t.Errorf("expected auto_unsubscribe_link_enabled false, got %v", fields["auto_unsubscribe_link_enabled"]) + } + if _, ok := fields["open_tracking_enabled"]; ok { + t.Error("expected unset open-tracking flag to be omitted from the body") + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "id": 1, "domain_name": "example.com", "dns_verified": true, "compliance_status": "compliant", + "tracking_opt_out_enabled": true, "auto_unsubscribe_link_enabled": false, + }) + }) + defer cleanup() + + cmd := domains.NewCmdDomains(f) + cmd.SetArgs([]string{"update", "--id", "1", "--tracking-opt-out=true", "--auto-unsubscribe-link=false"}) + cmd.SetOut(buf) + + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + output := buf.String() + if !strings.Contains(output, "TRACKING OPT OUT") { + t.Errorf("expected output to contain the tracking opt out column, got:\n%s", output) + } +} + +func TestDomainsUpdateMissingID(t *testing.T) { + f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) { + t.Error("expected no request to be sent") + }) + defer cleanup() + + cmd := domains.NewCmdDomains(f) + cmd.SetArgs([]string{"update", "--tracking-opt-out=true"}) + cmd.SetOut(buf) + + err := cmd.Execute() + if err == nil { + t.Fatal("expected error for missing --id") + } + if !strings.Contains(err.Error(), "--id is required") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestDomainsUpdateNoFields(t *testing.T) { + f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) { + t.Error("expected no request to be sent") + }) + defer cleanup() + + cmd := domains.NewCmdDomains(f) + cmd.SetArgs([]string{"update", "--id", "1"}) + cmd.SetOut(buf) + + err := cmd.Execute() + if err == nil { + t.Fatal("expected error when no attribute flag is passed") + } + if !strings.Contains(err.Error(), "at least one attribute flag is required") { + t.Errorf("unexpected error: %v", err) + } +} diff --git a/internal/commands/domains/list.go b/internal/commands/domains/list.go index 7c6c0ad..16cfd94 100644 --- a/internal/commands/domains/list.go +++ b/internal/commands/domains/list.go @@ -11,12 +11,16 @@ import ( ) type Domain struct { - ID int `json:"id"` - DomainName string `json:"domain_name"` - DNSVerified bool `json:"dns_verified"` - ComplianceStatus string `json:"compliance_status"` - InboundEnabled bool `json:"inbound_enabled"` - InboundVerified bool `json:"inbound_verified"` + ID int `json:"id"` + DomainName string `json:"domain_name"` + DNSVerified bool `json:"dns_verified"` + ComplianceStatus string `json:"compliance_status"` + InboundEnabled bool `json:"inbound_enabled"` + InboundVerified bool `json:"inbound_verified"` + OpenTrackingEnabled bool `json:"open_tracking_enabled"` + ClickTrackingEnabled bool `json:"click_tracking_enabled"` + TrackingOptOutEnabled bool `json:"tracking_opt_out_enabled"` + AutoUnsubscribeLinkEnabled bool `json:"auto_unsubscribe_link_enabled"` } type domainListResponse struct { @@ -32,6 +36,14 @@ var domainColumns = []output.Column{ {Header: "INBOUND VERIFIED", Field: "inbound_verified"}, } +var domainSettingsColumns = append( + append([]output.Column{}, domainColumns...), + output.Column{Header: "OPEN TRACKING", Field: "open_tracking_enabled"}, + output.Column{Header: "CLICK TRACKING", Field: "click_tracking_enabled"}, + output.Column{Header: "TRACKING OPT OUT", Field: "tracking_opt_out_enabled"}, + output.Column{Header: "AUTO UNSUBSCRIBE", Field: "auto_unsubscribe_link_enabled"}, +) + func NewCmdList(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "list", diff --git a/internal/commands/domains/update.go b/internal/commands/domains/update.go new file mode 100644 index 0000000..0006a42 --- /dev/null +++ b/internal/commands/domains/update.go @@ -0,0 +1,84 @@ +package domains + +import ( + "context" + "fmt" + + "github.com/mailtrap/mailtrap-cli/internal/client" + "github.com/mailtrap/mailtrap-cli/internal/cmdutil" + "github.com/mailtrap/mailtrap-cli/internal/config" + "github.com/mailtrap/mailtrap-cli/internal/output" + "github.com/spf13/cobra" +) + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + domainID string + openTracking bool + clickTracking bool + trackingOptOut bool + autoUnsubscribeLink bool + inboundEnabled bool + ) + + cmd := &cobra.Command{ + Use: "update", + Short: "Update a sending domain", + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.RequireFlag("id", domainID); err != nil { + return err + } + + c, err := f.NewClient() + if err != nil { + return err + } + + if _, err := config.RequireAccountID(); err != nil { + return err + } + + path := cmdutil.AccountPath("sending_domains", domainID) + + domainFields := map[string]interface{}{} + if cmd.Flags().Changed("open-tracking") { + domainFields["open_tracking_enabled"] = openTracking + } + if cmd.Flags().Changed("click-tracking") { + domainFields["click_tracking_enabled"] = clickTracking + } + if cmd.Flags().Changed("tracking-opt-out") { + domainFields["tracking_opt_out_enabled"] = trackingOptOut + } + if cmd.Flags().Changed("auto-unsubscribe-link") { + domainFields["auto_unsubscribe_link_enabled"] = autoUnsubscribeLink + } + if cmd.Flags().Changed("inbound-enabled") { + domainFields["inbound_enabled"] = inboundEnabled + } + + if len(domainFields) == 0 { + return fmt.Errorf("at least one attribute flag is required") + } + + body := map[string]interface{}{"sending_domain": domainFields} + + var domain Domain + if err := c.Patch(context.Background(), client.BaseGeneral, path, body, &domain); err != nil { + return err + } + + format := cmdutil.GetOutputFormat() + return output.Print(f.IOStreams.Out, format, domain, domainSettingsColumns) + }, + } + + cmd.Flags().StringVar(&domainID, "id", "", "Domain ID (required)") + cmd.Flags().BoolVar(&openTracking, "open-tracking", false, "Enable open tracking for emails sent from this domain") + cmd.Flags().BoolVar(&clickTracking, "click-tracking", false, "Enable click tracking for links in emails sent from this domain") + cmd.Flags().BoolVar(&trackingOptOut, "tracking-opt-out", false, "Enable the tracking opt-out link in tracked emails, requires open or click tracking") + cmd.Flags().BoolVar(&autoUnsubscribeLink, "auto-unsubscribe-link", false, "Automatically add an unsubscribe link to emails") + cmd.Flags().BoolVar(&inboundEnabled, "inbound-enabled", false, "Enable inbound email so the domain can be attached to an inbound inbox as a catch-all") + + return cmd +}