Skip to content
Merged
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ mailtrap inbound messages list --inbox-id 735 -o json | jq -r '.last_id // empty
| **Permissions** | `permissions resources`, `permissions bulk-update` |
| **Tokens** | `tokens list`, `tokens get`, `tokens create [--expires-at]`, `tokens delete`, `tokens reset [--expires-at]` |
| **Billing** | `billing usage` |
| **Organizations** | `organizations list-sub-accounts`, `organizations create-sub-account` |
| **Organizations** | `organizations list-sub-accounts`, `organizations create-sub-account`, `organizations delete-sub-account` |
| **Config** | `configure`, `completion [bash\|zsh\|fish\|powershell]` |

## Shell Completion
Expand Down
16 changes: 9 additions & 7 deletions docs/TEST_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,11 +325,13 @@ Prerequisite: Send an email with an attachment to the sandbox.

| # | Test | Command | Expected |
|---|------|---------|----------|
| 20.1 | List sub-accounts | `mailtrap organizations list-sub-accounts` | Table with sub-accounts |
| 20.2 | Create sub-account | `mailtrap organizations create-sub-account --name "test-sub"` | New sub-account |
| 20.3 | Missing name | `mailtrap organizations create-sub-account` | Error: `--name is required` |
| 20.1 | List sub-accounts | `mailtrap organizations list-sub-accounts --org-id <ORG_ID>` | Table with sub-accounts |
| 20.2 | Create sub-account | `mailtrap organizations create-sub-account --org-id <ORG_ID> --name "test-sub"` | New sub-account |
| 20.3 | Missing name | `mailtrap organizations create-sub-account --org-id <ORG_ID>` | Error: `--name is required` |
| 20.4 | Delete sub-account | `mailtrap organizations delete-sub-account --org-id <ORG_ID> --sub-account-id <NEW_ID>` | Success message; repeated call returns 404 |
| 20.5 | Delete with a list of IDs | `mailtrap organizations delete-sub-account --org-id <ORG_ID> --sub-account-id 12347,12348` | Error: `invalid argument`; nothing deleted |

**Caution:** Creating sub-accounts may have billing implications.
**Caution:** Creating sub-accounts may have billing implications. Deleting a sub-account is permanent and removes all of its data; deleting the organization's last sub-account also deletes the organization. Only delete the sub-account created in 20.2.

## 21. Configure

Expand Down Expand Up @@ -416,7 +418,7 @@ Run tests in dependency order so earlier tests create resources needed by later
17. **Permissions** (read/update)
18. **Tokens** (CRUD — may need admin token)
19. **Billing** (read-only)
20. **Organizations** (read-only, skip create unless safe)
20. **Organizations** (read-only, skip create/delete unless safe – delete only the sub-account created in 20.2)
21. **Configure** (local config only)
22. **Inbound** (CRUD for folders/inboxes/forward rules; messages/threads need received mail)

Expand Down Expand Up @@ -445,7 +447,7 @@ Run tests in dependency order so earlier tests create resources needed by later
| Permissions | 2 | 3 |
| Tokens | 5 | 6 |
| Billing | 1 | 2 |
| Organizations | 2 | 3 |
| Organizations | 3 | 5 |
| Configure | 1 | 2 |
| Inbound | 24 | 30 |
| **Total** | **~114** | **~137** |
| **Total** | **~115** | **~139** |
62 changes: 62 additions & 0 deletions internal/commands/organizations/delete_sub_account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package organizations

import (
"context"
"fmt"

"github.com/mailtrap/mailtrap-cli/internal/client"
"github.com/mailtrap/mailtrap-cli/internal/cmdutil"
"github.com/spf13/cobra"
)

func NewCmdDeleteSubAccount(f *cmdutil.Factory) *cobra.Command {
var orgID int64
var subAccountID int64

cmd := &cobra.Command{
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Use: "delete-sub-account",
Args: cobra.NoArgs,
Short: "Delete a sub-account from an organization",
Long: `Delete a sub-account from an organization. Requires organization admin permissions.
The organization must have the Organization & Sub-accounts feature enabled; otherwise the command fails with 403.

The deletion is permanent: the sub-account and all of its data are removed and cannot be restored.
Deleting the organization's last sub-account also deletes the organization.

A repeated call for the same sub-account returns 404. A concurrent deletion of the same sub-account may fail with 409. Rate limit: 10 requests per minute per organization.`,
RunE: func(cmd *cobra.Command, args []string) error {
if !cmd.Flags().Changed("org-id") {
return fmt.Errorf("--org-id is required")
}
if orgID <= 0 {
return fmt.Errorf("--org-id must be greater than 0")
}
if !cmd.Flags().Changed("sub-account-id") {
return fmt.Errorf("--sub-account-id is required")
}
if subAccountID <= 0 {
return fmt.Errorf("--sub-account-id must be greater than 0")
}

c, err := f.NewClient()
if err != nil {
return err
}

path := fmt.Sprintf("/api/organizations/%d/sub_accounts/%d", orgID, subAccountID)

if err := c.Delete(context.Background(), client.BaseGeneral, path, nil); err != nil {
return err
}

fmt.Fprintf(f.IOStreams.Out, "Sub-account %d deleted successfully\n", subAccountID)

return nil
},
}

cmd.Flags().Int64Var(&orgID, "org-id", 0, "Organization ID")
cmd.Flags().Int64Var(&subAccountID, "sub-account-id", 0, "Sub-account ID")

return cmd
}
1 change: 1 addition & 0 deletions internal/commands/organizations/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func NewCmdOrganizations(f *cmdutil.Factory) *cobra.Command {

cmd.AddCommand(NewCmdListSubAccounts(f))
cmd.AddCommand(NewCmdCreateSubAccount(f))
cmd.AddCommand(NewCmdDeleteSubAccount(f))

return cmd
}
117 changes: 117 additions & 0 deletions internal/commands/organizations/organizations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,120 @@ func TestCreateSubAccountMissingFlags(t *testing.T) {
t.Fatal("expected error when required flags are missing")
}
}

func TestDeleteSubAccount(t *testing.T) {
f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method)
}
if !strings.HasSuffix(r.URL.Path, "/api/organizations/456/sub_accounts/789") {
t.Errorf("unexpected path: %s", r.URL.Path)
}

w.WriteHeader(http.StatusNoContent)
})
defer cleanup()

cmd := organizations.NewCmdOrganizations(f)
cmd.SetArgs([]string{"delete-sub-account", "--org-id", "456", "--sub-account-id", "789"})
cmd.SetOut(buf)

err := cmd.Execute()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

output := buf.String()
if !strings.Contains(output, "Sub-account 789 deleted successfully") {
t.Errorf("expected output to contain 'Sub-account 789 deleted successfully', got:\n%s", output)
}
}

func TestDeleteSubAccountAPIError(t *testing.T) {
f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"error": "Not Found"})
})
defer cleanup()

cmd := organizations.NewCmdOrganizations(f)
cmd.SetArgs([]string{"delete-sub-account", "--org-id", "456", "--sub-account-id", "789"})
cmd.SetOut(buf)

err := cmd.Execute()
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "API error 404") {
t.Errorf("expected error to contain 'API error 404', got: %v", err)
}
if strings.Contains(buf.String(), "deleted successfully") {
t.Errorf("expected no success message, got:\n%s", buf.String())
}
}

func TestDeleteSubAccountInvalidFlags(t *testing.T) {
cases := []struct {
name string
args []string
want string
}{
{
name: "missing org id",
args: []string{"delete-sub-account", "--sub-account-id", "789"},
want: "--org-id is required",
},
{
name: "missing sub-account id",
args: []string{"delete-sub-account", "--org-id", "456"},
want: "--sub-account-id is required",
},
{
name: "list of sub-account ids",
args: []string{"delete-sub-account", "--org-id", "456", "--sub-account-id", "12347,12348"},
want: `invalid argument "12347,12348" for "--sub-account-id"`,
},
{
name: "non-numeric org id",
args: []string{"delete-sub-account", "--org-id", "abc", "--sub-account-id", "789"},
want: `invalid argument "abc" for "--org-id"`,
},
{
name: "non-positive sub-account id",
args: []string{"delete-sub-account", "--org-id", "456", "--sub-account-id", "0"},
want: "--sub-account-id must be greater than 0",
},
{
name: "non-positive org id",
args: []string{"delete-sub-account", "--org-id", "0", "--sub-account-id", "789"},
want: "--org-id must be greater than 0",
},
{
name: "extra positional argument",
args: []string{"delete-sub-account", "--org-id", "456", "--sub-account-id", "789", "790"},
want: `unknown command "790"`,
},
}

for _, tc := range cases {
t.Run(tc.name, func(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 := organizations.NewCmdOrganizations(f)
cmd.SetArgs(tc.args)
cmd.SetOut(buf)

err := cmd.Execute()
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), tc.want) {
t.Errorf("expected error to contain %q, got: %v", tc.want, err)
}
})
}
}
9 changes: 9 additions & 0 deletions skill-evals/mailtrap-cli/evals.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,15 @@
"must_not_contain": ["accounts", "list-sub-accounts"]
}
},
{
"name": "delete_sub_account",
"prompt": "Remove sub-account 12347 from organization 2000002",
"expected": {
"command": "organizations delete-sub-account",
"flags": ["--org-id", "--sub-account-id"],
"must_not_contain": ["accounts delete", "account-access remove"]
}
},
{
"name": "send_with_cc_bcc",
"prompt": "Send a transactional email from boss@acme.com to team@acme.com, CC manager@acme.com, BCC hr@acme.com with subject 'Update'",
Expand Down
1 change: 1 addition & 0 deletions skills/mailtrap-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,4 @@ A `null` cursor means there are no more pages. `suppressions list` and `messages
- Message inspection commands (`messages html`, `messages raw`, etc.) require both `--sandbox-id` and `--id`
- The `configure` command validates the token against the API before saving
- Domain verification is handled through the Mailtrap web dashboard, not the CLI
- `organizations delete-sub-account` is permanent, and deleting the organization's last sub-account also deletes the organization. Confirm with the user first, and run `organizations list-sub-accounts --org-id <id>` to check it isn't the last one
13 changes: 13 additions & 0 deletions skills/mailtrap-cli/references/accounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,16 @@ Create a sub-account under an organization.
|------|------|----------|-------------|
| `--org-id` | string | Yes | Organization ID |
| `--name` | string | Yes | Sub-account name |

---

## organizations delete-sub-account

Delete a sub-account from an organization. Requires organization admin permissions. The organization must have the Organization & Sub-accounts feature enabled; otherwise the command fails with 403.

| Flag | Type | Required | Description |
|------|------|----------|-------------|
| `--org-id` | int | Yes | Organization ID |
| `--sub-account-id` | int | Yes | Sub-account ID |

**Caution:** Permanent – the sub-account and all of its data are removed and cannot be restored. Deleting the organization's last sub-account also deletes the organization. A repeated call for the same sub-account returns 404. A concurrent deletion of the same sub-account may fail with 409. Rate limit: 10 requests per minute per organization.
Loading