Skip to content

MT-22401: Add email campaigns commands - #9

Draft
Rabsztok wants to merge 3 commits into
mainfrom
MT-22401-cli-email-campaigns
Draft

MT-22401: Add email campaigns commands#9
Rabsztok wants to merge 3 commits into
mainfrom
MT-22401-cli-email-campaigns

Conversation

@Rabsztok

@Rabsztok Rabsztok commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Draft until the server-side Email Campaigns API changes are released.

Motivation

MT-22401

Port the Email Campaigns public API (MT-21113) to the CLI.

Changes

  • Add mailtrap email-campaigns with 12 subcommands: list (--per-page/--search/--token), get, create, update (PATCH semantics — only changed flags are sent), delete, the five lifecycle actions (start, schedule --datetime, cancel, terminate, reset), and stats (--start-date/--end-date)
  • Requests follow the published OpenAPI contract: flat bodies, data-envelope unwrapping, UUID --mailsend-domain-id, nested reply_to/template_attributes/delivery_options built from flat flags, audience id slices
  • The endpoint is token-scoped, so these commands don't require a configured account id (unlike other resource groups)
  • README command-table row + examples under "Manage resources"

How to test

  • mailtrap email-campaigns create --name "Test" --mailsend-domain-id <uuid> --from-local-part news --subject "Hi" with a real token — expect a draft campaign row
  • Walk the lifecycle: schedule --datetime <future ISO>cancelstartterminate; verify state transitions and that invalid transitions print the API error
  • email-campaigns list --search <name> and stats <id> --start-date ... --end-date ... return sensible tables and --output json parses

Summary by CodeRabbit

  • New Features
    • Added comprehensive email campaign management to the CLI.
    • Create, view, list, update, and delete campaigns.
    • Start, schedule, cancel, terminate, and reset campaigns.
    • View campaign performance statistics with optional date filters.
    • Supports campaign content, sender details, targeting, delivery settings, and multiple output formats.
  • Documentation
    • Added email campaign usage examples and command reference documentation.
  • Tests
    • Added coverage for campaign management, lifecycle actions, statistics, output formats, and validation.

Decisions:
- Request bodies are flat (no email_campaign wrapper); nested reply_to/template_attributes are assembled from flat flags
- Single, list, and stats responses unwrap the data envelope; list pagination metadata is ignored for output
- Endpoint is token-scoped (/api/email_campaigns) — no account-id requirement, unlike other resources
- Lifecycle actions (start/schedule/cancel/terminate/reset) are subcommands sharing one body-less POST helper
- Delete handles 204 No Content with a confirmation message per repo convention
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new email-campaigns CLI command group with campaign listing, retrieval, creation, updates, deletion, lifecycle actions, scheduling, statistics, tests, root-command registration, and README usage documentation.

Changes

Email campaigns

Layer / File(s) Summary
Campaign contracts and read commands
internal/commands/emailcampaigns/emailcampaigns.go, internal/commands/emailcampaigns/list.go, internal/commands/emailcampaigns/get.go, internal/commands/emailcampaigns/emailcampaigns_test.go
Defines token-scoped campaign endpoints, response models, table columns, and list/get commands with table and JSON output coverage.
Campaign creation and updates
internal/commands/emailcampaigns/attributes.go, internal/commands/emailcampaigns/create.go, internal/commands/emailcampaigns/update.go, internal/commands/emailcampaigns/emailcampaigns_test.go
Adds shared campaign flags and conditional nested payload construction for create POST requests and update PATCH requests, including required-flag and omission tests.
Campaign operations and statistics
internal/commands/emailcampaigns/delete.go, internal/commands/emailcampaigns/start.go, internal/commands/emailcampaigns/cancel.go, internal/commands/emailcampaigns/terminate.go, internal/commands/emailcampaigns/reset.go, internal/commands/emailcampaigns/schedule.go, internal/commands/emailcampaigns/stats.go, internal/commands/emailcampaigns/emailcampaigns_test.go
Adds deletion, lifecycle transitions, scheduling with datetime payloads, and date-filtered count/rate statistics commands with endpoint and output tests.
CLI integration and documentation
cmd/root.go, README.md, internal/commands/emailcampaigns/emailcampaigns.go, internal/commands/emailcampaigns/schedule.go
Registers the email campaigns command group in the root CLI and documents campaign commands and examples.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant EmailCampaignCommands
  participant APIClient
  participant EmailCampaignAPI
  participant Output
  CLI->>EmailCampaignCommands: execute campaign command
  EmailCampaignCommands->>APIClient: create authenticated client
  APIClient->>EmailCampaignAPI: request campaign resource or action
  EmailCampaignAPI-->>APIClient: campaign or statistics response
  APIClient-->>EmailCampaignCommands: decoded response
  EmailCampaignCommands->>Output: render configured output
  Output-->>CLI: formatted result
Loading

Suggested reviewers: leonid-shevtsov, vladimirtaytor

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding email campaigns CLI commands.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Rabsztok
Rabsztok marked this pull request as ready for review July 30, 2026 12:03

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/commands/emailcampaigns/list.go`:
- Line 121: Update the perPage flag binding in the campaign list command to use
50 as its default, matching the advertised help text, while preserving the
existing Changed check and surrounding behavior.

In `@internal/commands/emailcampaigns/start.go`:
- Around line 14-33: Update the API request flow in newLifecycleCmd and the
underlying internal/client.Client configuration so Email Campaigns calls cannot
block indefinitely. Replace the unbounded context.Background() request context
with an appropriate deadline or configure the HTTP client with a suitable
timeout, preserving the existing campaignPath and response handling.

In `@internal/commands/emailcampaigns/update.go`:
- Around line 29-32: Update the email-campaign update flow around
buildAttributesBody and the Patch call to validate that at least one mutable
attribute was provided; return a validation error before issuing Patch when the
body contains no changes. Add a regression test covering an update with only
--id and verify no request is sent.

In `@README.md`:
- Line 129: Update the README schedule command’s --datetime value to a future or
clearly placeholder timestamp, and ensure the corresponding Mailtrap app
examples include a matching schedule example.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8b5236ce-3368-40c0-9482-c3adc59cede6

📥 Commits

Reviewing files that changed from the base of the PR and between 1c2f517 and de151d1.

📒 Files selected for processing (16)
  • README.md
  • cmd/root.go
  • internal/commands/emailcampaigns/attributes.go
  • internal/commands/emailcampaigns/cancel.go
  • internal/commands/emailcampaigns/create.go
  • internal/commands/emailcampaigns/delete.go
  • internal/commands/emailcampaigns/emailcampaigns.go
  • internal/commands/emailcampaigns/emailcampaigns_test.go
  • internal/commands/emailcampaigns/get.go
  • internal/commands/emailcampaigns/list.go
  • internal/commands/emailcampaigns/reset.go
  • internal/commands/emailcampaigns/schedule.go
  • internal/commands/emailcampaigns/start.go
  • internal/commands/emailcampaigns/stats.go
  • internal/commands/emailcampaigns/terminate.go
  • internal/commands/emailcampaigns/update.go

Comment thread internal/commands/emailcampaigns/list.go Outdated
Comment on lines +14 to +33
func newLifecycleCmd(f *cmdutil.Factory, action, short string) *cobra.Command {
var campaignID string

cmd := &cobra.Command{
Use: action,
Short: short,
RunE: func(cmd *cobra.Command, args []string) error {
if err := cmdutil.RequireFlag("id", campaignID); err != nil {
return err
}

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

var resp campaignResponse
if err := c.Post(context.Background(), client.BaseGeneral, campaignPath(campaignID, action), nil, &resp); err != nil {
return err
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether internal/client.Client sets a default request/dial timeout.
ast-grep outline internal/client --items all
rg -n -A5 -B5 'Timeout' internal/client

Repository: mailtrap/mailtrap-cli

Length of output: 1158


🏁 Script executed:

#!/bin/bash
set -u
printf '--- client.go ---\n'
cat -n internal/client/client.go
printf '\n--- relevant command timeout context usages ---\n'
rg -n 'context\.Background|context\.With|Timeout|Deadline|ctx' internal/client internal/commands/emailcampaigns

Repository: mailtrap/mailtrap-cli

Length of output: 10909


🌐 Web query:

Go net/http New default http.Client Timeout documentation

💡 Result:

The Go net/http package does not impose a default timeout on the standard http.Client [1][2]. The http.DefaultClient is initialized with a zero value for its Timeout field, which explicitly means "no timeout" [1][3]. In the Go documentation, the Timeout field of the http.Client struct is defined as follows [1][4]: Timeout specifies a time limit for requests made by this Client. The timeout includes connection time, any redirects, and reading the response body. A Timeout of zero means no timeout. Because http.DefaultClient has no timeout, using it or the convenience functions that rely on it (such as http.Get, http.Post, and http.Head) is generally discouraged for production applications, especially those interacting with the public internet, as it can lead to resource leaks and hung requests [5][6]. Developers are encouraged to define a custom http.Client with a specified Timeout or to use context.Context with requests to enforce deadlines [7][5].

Citations:


Add timeout coverage for context.Background() API calls.

internal/client.Client creates &http.Client{} with no Timeout, and these commands pass context.Background() by value, so Email Campaigns requests can block indefinitely if the API response hangs. Add a request deadline or configure the HTTP client with an appropriate timeout.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/commands/emailcampaigns/start.go` around lines 14 - 33, Update the
API request flow in newLifecycleCmd and the underlying internal/client.Client
configuration so Email Campaigns calls cannot block indefinitely. Replace the
unbounded context.Background() request context with an appropriate deadline or
configure the HTTP client with a suitable timeout, preserving the existing
campaignPath and response handling.

Comment on lines +29 to +32
body := buildAttributesBody(cmd, attrs)

var resp campaignResponse
if err := c.Patch(context.Background(), client.BaseGeneral, campaignPath(campaignID), body, &resp); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject updates with no changed attributes.

email-campaigns update --id 4567 builds an empty partial-update body and still issues PATCH. Return a validation error before the request when no mutable attribute flag was provided, and add a regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/commands/emailcampaigns/update.go` around lines 29 - 32, Update the
email-campaign update flow around buildAttributesBody and the Patch call to
validate that at least one mutable attribute was provided; return a validation
error before issuing Patch when the body contains no changes. Add a regression
test covering an update with only --id and verify no request is sent.

Comment thread README.md Outdated
Decisions:
- Bind --per-page default to 50 (matches API default per OpenAPI spec); Changed check kept so the param is only sent when set explicitly
- README schedule example now uses an obviously-future timestamp (2030-01-01)
- No timeout added for context.Background() calls: matches repo convention (no command uses timeouts); a CLI-wide timeout belongs in a separate change
- Empty updates not rejected: templates/webhooks update commands also send empty partial PATCH; consistency kept
@Rabsztok

Copy link
Copy Markdown
Contributor Author

Addressed CodeRabbit findings in 06ed4f5:

Fixed

  • list.go per-page default: bound the flag default to 50, matching the API default from the OpenAPI spec; the Changed check is kept so per_page is only sent when set explicitly, and Cobra now displays an accurate (default 50).
  • README schedule example: replaced the past 2026-06-01T09:00:00Z with an obviously-future placeholder 2030-01-01T09:00:00Z.

Skipped

  • start.go timeout for context.Background(): matches repo convention — no command in the CLI uses timeout contexts and the shared HTTP client has no timeout, so this is not campaigns-specific; a CLI-wide timeout belongs in a separate change.
  • update.go rejecting empty updates: sibling update commands (templates, webhooks) also issue PATCH with an empty partial body when no attribute flag is given; rejecting only for campaigns would diverge from established behavior.

go build ./... && go vet ./... && go test ./... all pass.

Decisions:
- The API treats contact_list_ids/contact_segment_ids as the full set,
  so an explicit [] clears them — pflag cannot parse an empty slice
  value, so dedicated --clear-contact-lists/--clear-contact-segments
  booleans express it; mutually exclusive with the ids flags
@Rabsztok

Copy link
Copy Markdown
Contributor Author

Follow-up from a cross-SDK contract audit: the API treats audience ids as the full set (explicit [] clears the audience), but pflag can't parse an empty --contact-list-ids= value, so clearing was inexpressible from the CLI. Added --clear-contact-lists / --clear-contact-segments to update (mutually exclusive with the ids flags), with tests asserting [] is sent and the conflict errors out. README example added.

@Rabsztok
Rabsztok marked this pull request as draft July 31, 2026 11:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant