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 +}