Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ mailtrap contact-lists list
mailtrap contact-lists list --search news
mailtrap contact-fields create --name "Company" --data-type text --merge-tag "{{company}}"

# Email campaigns
mailtrap email-campaigns list --search spring
mailtrap email-campaigns create --name "Spring Sale" --mailsend-domain-id "d2313359-acb4-4b87-bce6-f5774f6a1e37" --from-local-part news --subject "Spring is here"
mailtrap email-campaigns update --id 4567 --body-html '<h1>Hi {{first_name}}!</h1><a href="__unsubscribe_url__">Unsubscribe</a>' --contact-list-ids 55,56
mailtrap email-campaigns update --id 4567 --clear-contact-lists
mailtrap email-campaigns schedule --id 4567 --datetime "2030-01-01T09:00:00Z"
mailtrap email-campaigns stats --id 4567 --start-date 2026-05-01 --end-date 2026-05-31

# Sandboxes & projects
mailtrap projects list
mailtrap sandboxes list
Expand Down Expand Up @@ -155,6 +163,7 @@ mailtrap domains list --output text
| **Contacts** | `contacts get`, `contacts create`, `contacts update`, `contacts delete`, `contacts import`, `contacts export`, `contacts import-status`, `contacts export-status`, `contacts create-event` |
| **Contact Lists** | `contact-lists list`, `contact-lists get`, `contact-lists create`, `contact-lists update`, `contact-lists delete` |
| **Contact Fields** | `contact-fields list`, `contact-fields get`, `contact-fields create`, `contact-fields update`, `contact-fields delete` |
| **Email Campaigns** | `email-campaigns list`, `email-campaigns get`, `email-campaigns create`, `email-campaigns update`, `email-campaigns delete`, `email-campaigns start`, `email-campaigns schedule`, `email-campaigns cancel`, `email-campaigns terminate`, `email-campaigns reset`, `email-campaigns stats` |
| **Projects** | `projects list`, `projects get`, `projects create`, `projects update`, `projects delete` |
| **Sandboxes** | `sandboxes list`, `sandboxes get`, `sandboxes create`, `sandboxes update`, `sandboxes delete`, `sandboxes clean`, `sandboxes mark-read`, `sandboxes reset-credentials`, `sandboxes toggle-email`, `sandboxes reset-email` |
| **Messages** | `messages list`, `messages get`, `messages update`, `messages delete`, `messages forward`, `messages spam-score`, `messages html-analysis`, `messages headers`, `messages html`, `messages text`, `messages source`, `messages raw`, `messages eml` |
Expand Down
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"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/sandboxes"
"github.com/mailtrap/mailtrap-cli/internal/commands/messages"
"github.com/mailtrap/mailtrap-cli/internal/commands/organizations"
Expand Down Expand Up @@ -70,6 +71,7 @@ func NewRootCmd(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(contacts.NewCmdContacts(f))
cmd.AddCommand(contact_lists.NewCmdContactLists(f))
cmd.AddCommand(contact_fields.NewCmdContactFields(f))
cmd.AddCommand(emailcampaigns.NewCmdEmailCampaigns(f))

// Account Management
cmd.AddCommand(accounts.NewCmdAccounts(f))
Expand Down
107 changes: 107 additions & 0 deletions internal/commands/emailcampaigns/attributes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package emailcampaigns

import (
"github.com/spf13/cobra"
)

// campaignAttrs holds the writable campaign attributes shared by create and update.
type campaignAttrs struct {
Name string
MailsendDomainID string
FromDisplayName string
FromLocalPart string
ReplyToDisplayName string
ReplyToLocalPart string
ReplyToDomain string
Subject string
BodyHTML string
BodyText string
MergeTags []string
DeliveryMode string
EmailsPerHour int64
ContactListIDs []int64
ContactSegmentIDs []int64
}

func addAttributeFlags(cmd *cobra.Command, attrs *campaignAttrs) {
cmd.Flags().StringVar(&attrs.Name, "name", "", "Campaign name")
cmd.Flags().StringVar(&attrs.MailsendDomainID, "mailsend-domain-id", "", "UUID of the verified sending domain")
cmd.Flags().StringVar(&attrs.FromDisplayName, "from-display-name", "", "Display name shown in the From header")
cmd.Flags().StringVar(&attrs.FromLocalPart, "from-local-part", "", "Local part (before the @) of the From address")
cmd.Flags().StringVar(&attrs.ReplyToDisplayName, "reply-to-display-name", "", "Reply-To display name")
cmd.Flags().StringVar(&attrs.ReplyToLocalPart, "reply-to-local-part", "", "Reply-To local part (before the @)")
cmd.Flags().StringVar(&attrs.ReplyToDomain, "reply-to-domain", "", "Reply-To domain")
cmd.Flags().StringVar(&attrs.Subject, "subject", "", "Email subject line")
cmd.Flags().StringVar(&attrs.BodyHTML, "body-html", "", "HTML body of the email (the design)")
cmd.Flags().StringVar(&attrs.BodyText, "body-text", "", "Plain-text alternative of the email body")
cmd.Flags().StringSliceVar(&attrs.MergeTags, "merge-tags", nil, "Merge tag names used in the content (comma-separated), without {{ }}")
cmd.Flags().StringVar(&attrs.DeliveryMode, "delivery-mode", "", "Delivery mode: rapid, gradual")
cmd.Flags().Int64Var(&attrs.EmailsPerHour, "emails-per-hour", 0, "Emails per hour when delivery mode is gradual")
cmd.Flags().Int64SliceVar(&attrs.ContactListIDs, "contact-list-ids", nil, "Contact list IDs to send to (comma-separated)")
cmd.Flags().Int64SliceVar(&attrs.ContactSegmentIDs, "contact-segment-ids", nil, "Contact segment IDs to send to (comma-separated)")
}

// buildAttributesBody builds the flat request body (no wrapper key) from the flags
// that were explicitly set, assembling the nested reply_to and template_attributes objects.
func buildAttributesBody(cmd *cobra.Command, attrs *campaignAttrs) map[string]interface{} {
body := map[string]interface{}{}

if cmd.Flags().Changed("name") {
body["name"] = attrs.Name
}
if cmd.Flags().Changed("mailsend-domain-id") {
body["mailsend_domain_id"] = attrs.MailsendDomainID
}
if cmd.Flags().Changed("from-display-name") {
body["from_display_name"] = attrs.FromDisplayName
}
if cmd.Flags().Changed("from-local-part") {
body["from_local_part"] = attrs.FromLocalPart
}

replyTo := map[string]interface{}{}
if cmd.Flags().Changed("reply-to-display-name") {
replyTo["display_name"] = attrs.ReplyToDisplayName
}
if cmd.Flags().Changed("reply-to-local-part") {
replyTo["local_part"] = attrs.ReplyToLocalPart
}
if cmd.Flags().Changed("reply-to-domain") {
replyTo["domain"] = attrs.ReplyToDomain
}
if len(replyTo) > 0 {
body["reply_to"] = replyTo
}

templateAttrs := map[string]interface{}{}
if cmd.Flags().Changed("subject") {
templateAttrs["subject"] = attrs.Subject
}
if cmd.Flags().Changed("body-html") {
templateAttrs["body_html"] = attrs.BodyHTML
}
if cmd.Flags().Changed("body-text") {
templateAttrs["body_text"] = attrs.BodyText
}
if cmd.Flags().Changed("merge-tags") {
templateAttrs["merge_tags"] = attrs.MergeTags
}
if len(templateAttrs) > 0 {
body["template_attributes"] = templateAttrs
}

if cmd.Flags().Changed("delivery-mode") {
body["delivery_mode"] = attrs.DeliveryMode
}
if cmd.Flags().Changed("emails-per-hour") {
body["delivery_options"] = map[string]interface{}{"emails_per_hour": attrs.EmailsPerHour}
}
if cmd.Flags().Changed("contact-list-ids") {
body["contact_list_ids"] = attrs.ContactListIDs
}
if cmd.Flags().Changed("contact-segment-ids") {
body["contact_segment_ids"] = attrs.ContactSegmentIDs
}

return body
}
10 changes: 10 additions & 0 deletions internal/commands/emailcampaigns/cancel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package emailcampaigns

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

func NewCmdCancel(f *cmdutil.Factory) *cobra.Command {
return newLifecycleCmd(f, "cancel", "Cancel a scheduled email campaign")
}
56 changes: 56 additions & 0 deletions internal/commands/emailcampaigns/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package emailcampaigns

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 {
attrs := &campaignAttrs{}

cmd := &cobra.Command{
Use: "create",
Short: "Create an email campaign",
RunE: func(cmd *cobra.Command, args []string) error {
if err := cmdutil.RequireFlag("name", attrs.Name); err != nil {
return err
}
if err := cmdutil.RequireFlag("mailsend-domain-id", attrs.MailsendDomainID); err != nil {
return err
}
if err := cmdutil.RequireFlag("from-local-part", attrs.FromLocalPart); err != nil {
return err
}
if err := cmdutil.RequireFlag("subject", attrs.Subject); err != nil {
return err
}

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

body := buildAttributesBody(cmd, attrs)

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

format := cmdutil.GetOutputFormat()
return output.Print(f.IOStreams.Out, format, resp.Data, campaignColumns)
},
}

addAttributeFlags(cmd, attrs)
// The attribute flags are shared with update, where all of them are optional.
for _, name := range []string{"name", "mailsend-domain-id", "from-local-part", "subject"} {
cmd.Flags().Lookup(name).Usage += " (required)"
}

return cmd
}
41 changes: 41 additions & 0 deletions internal/commands/emailcampaigns/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package emailcampaigns

import (
"context"
"fmt"

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

func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
var campaignID string

cmd := &cobra.Command{
Use: "delete",
Short: "Delete an email campaign",
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
}

// The API returns 204 No Content with no body.
if err := c.Delete(context.Background(), client.BaseGeneral, campaignPath(campaignID), nil); err != nil {
return err
}

fmt.Fprintln(f.IOStreams.Out, "Email campaign deleted successfully.")
return nil
},
}

cmd.Flags().StringVar(&campaignID, "id", "", "Email campaign ID (required)")

return cmd
}
39 changes: 39 additions & 0 deletions internal/commands/emailcampaigns/emailcampaigns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package emailcampaigns

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

// Email campaigns are token-scoped: the API path is /api/email_campaigns with no
// account_id segment, so commands in this package do not call config.RequireAccountID().
const basePath = "/api/email_campaigns"

func campaignPath(segments ...string) string {
path := basePath
for _, s := range segments {
path += "/" + s
}
return path
}

func NewCmdEmailCampaigns(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "email-campaigns",
Short: "Manage email campaigns",
}

cmd.AddCommand(NewCmdList(f))
cmd.AddCommand(NewCmdGet(f))
cmd.AddCommand(NewCmdCreate(f))
cmd.AddCommand(NewCmdUpdate(f))
cmd.AddCommand(NewCmdDelete(f))
cmd.AddCommand(NewCmdStart(f))
cmd.AddCommand(NewCmdSchedule(f))
cmd.AddCommand(NewCmdCancel(f))
cmd.AddCommand(NewCmdTerminate(f))
cmd.AddCommand(NewCmdReset(f))
cmd.AddCommand(NewCmdStats(f))

return cmd
}
Loading
Loading