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