Skill: leadsnavideveloper/mailgo-coldmail-marketing (version 1.1.2)

--- name: mailgo-campaign-suite description: Complete cold email campaign suite for Mailgo — verify recipients, claim free mailbox, generate & optimize content, create campaigns, manage lifecycle, and view reports. All-in-one skill that handles the full outreach pipeline end-to-end. Use when a user wants to send cold emails, launch outreach campaigns, or manage existing campaigns. env: MAILGO_API_KEY: required: true description: > Mailgo OpenAPI Key. Obtain from https://app.mailgo.ai → Click avatar in bottom-left corner → Personal Tokens → Create Token. Used as X-API-Key header for all Mailgo API calls (api.leadsnavi.com). Never paste into chat — set as local env var only. dependencies: python: ">=3.7" optional: - openpyxl # for .xlsx file support --- # Mailgo Campaign Suite One skill, complete cold email pipeline. From recipient verification to campaign reporting — everything runs through bundled scripts with zero third-party dependencies. --- ## Step 0 — Authentication Setup **Required:** `MAILGO_API_KEY` environment variable must be set before any other step. ### Quick check ```bash # Confirm the variable is set (shows first 5 characters only) echo "${MAILGO_API_KEY:0:5}" ``` If output is non-empty → proceed to Step 1. If empty → follow the setup flow below. ### Setup Flow **Sub-step 0.1 — Register or Log In** New users: 1. Go to [https://app.mailgo.ai](https://app.mailgo.ai) 2. Click "Sign Up" and complete registration Existing users: 1. Go to [https://app.mailgo.ai](https://app.mailgo.ai) 2. Log in with your credentials **Sub-step 0.2 — Create Personal Token** Once logged in: 1. Click your **avatar** in the bottom-left corner 2. Select **Personal Tokens** from the menu 3. Click **Create Token** 4. Give your token a descriptive name (e.g., "Claude Code") 5. Copy the generated token > **SECURITY:** Never paste the token into chat. It must only be set as a local environment variable. **Sub-step 0.3 — Set Environment Variable** ```bash # macOS / Linux (permanent) echo 'export MAILGO_API_KEY="YOUR_TOKEN"' >> ~/.zshrc && source ~/.zshrc # Windows PowerShell (permanent) [System.Environment]::SetEnvironmentVariable('MAILGO_API_KEY', 'YOUR_TOKEN', 'User') ``` Replace `YOUR_TOKEN` with the copied value. **Sub-step 0.4 — Verify** ```bash # macOS / Linux — shows first 5 characters to confirm without exposing the token echo "${MAILGO_API_KEY:0:5}" # Windows PowerShell $k = $env:MAILGO_API_KEY; if ($k) { $k.Substring(0, [Math]::Min(5, $k.Length)) } else { "" } ``` If output is non-empty, you're ready. If empty, run `source ~/.zshrc` or open a new terminal. ### Key Facts | Item | Value | |------|-------| | Token source | app.mailgo.ai → Avatar → Personal Tokens | | Env variable name | `MAILGO_API_KEY` | | API header | `X-[REDACTED_SECRET]` | | Token scope | Your Mailgo account (mailboxes, campaigns, reports) | | Revoke token | app.mailgo.ai → Avatar → Personal Tokens → Delete | ### Auth Error Handling | Issue | Fix | |-------|-----| | Empty after `source ~/.zshrc` | Open a new terminal window | | 401 Unauthorized | Token may be invalid or deleted — create a new one in Personal Tokens | | 403 Forbidden | Ensure `User-Agent` header is set (scripts handle this automatically) | --- ## Step 0.5 — Upfront Information Gathering **Collect all required information in a single interaction before starting the pipeline.** Ask all questions at once. Do not ask in separate rounds. The user fills in what they know; mark anything skipped as "not provided". > **When to run:** Only when the user intends to create a new campaign. Skip for report/manage/replies tasks. --- ### Group A — Your Information (for email signature & credibility) Ask all of these together in one message: | # | Question | Field | Required? | |---|----------|-------|-----------| | 1 | What is your **company name**? | `sender_company` | Yes | | 2 | What is **your name** (for the signature)? | `sender_name` | Yes | | 3 | What does your company **offer** (products / services)? | `sender_offerings` | Yes | | 4 | What is your **job title**? | `sender_title` | Recommended | | 5 | What is your company **website**? | `sender_website` | Recommended | | 6 | Any notable **clients, certifications, or metrics** to mention? | `sender_proof` | Optional | --- ### Group B — Your Recipients Ask all of these together in the same message as Group A: | # | Question | Field | |---|----------|-------| | 7 | Who are you sending to? Paste emails, or provide a file path (CSV / XLSX / TXT). | `recipients` | | 8 | What is the **goal** of this campaign? (one sentence) | `campaign_purpose` | | 9 | Do your recipients have **names** available? (column name or yes/no) | `has_name` | | 10 | Do you have recipients' **company names**? (column name or yes/no) | `has_company` | | 11 | Do you have recipients' **job titles**? (column name or yes/no) | `has_title` | | 12 | Do you have recipients' **company websites / domains**? (column name or yes/no) | `has_domain` | > If the user provides a file, auto-detect columns after reading the file headers — then mark fields 9–12 automatically without asking again. > > **⚠ Header Warning:** Column detection relies entirely on header names. Files without headers (e.g. a bare two-column XLSX with email + company but no row of column names) will only have `col_0`, `col_1`, … as synthetic headers — none of which match any known field name. As a result, **only the email column can be identified by content scanning; all other columns (company, name, title, domain) will be silently discarded.** > Whenever a user provides a headerless file and you can see that extra columns exist (e.g. "two-column spreadsheet"), **proactively warn them**: > _"Your file appears to have no header row. Only email addresses were imported — company names and other fields were not recognized. Add a header row (e.g. `email`, `company`, `name`) and re-upload to include that data."_ --- ### Auto-Derive Name from Email Prefix (CRITICAL) **Immediately after receiving the recipient list**, check whether name data is available. If `has_name` is false (no name column detected, no names provided inline): 1. Extract name from each email's local-part (the portion before `@`): - Split on `.`, `-`, `_` → capitalize each segment → join with space - Examples: `[REDACTED_SECRET]` → `"Alice Smith"`, `[REDACTED_SECRET]` → `"John Doe"`, `[REDACTED_SECRET]` → `"Jdoe"` 2. Store the derived name into each recipient entry in `session_context.recipients` 3. Set `recipient_fields.name = true` with source noted as `"auto-derived from email prefix"` 4. **Do this silently** — do not ask the user, do not announce it; just note it in the Step 3 change summary This ensures `#{Name}` is **always available** as long as there are valid email addresses. --- ### Handling Partial Answers | Situation | Action | |-----------|--------| | User skips a **required** field (1, 2, 3, 7, 8) | Ask once more specifically for that field only | | User skips a **recommended** field (4, 5) | Mark as not provided, do not ask again | | User skips an **optional** field (6) | Mark as not provided, move on | | User says "I'll send to a file I haven't uploaded yet" | Ask for file path, then detect columns | | User provides inline recipients with no extra data | Auto-derive names from email prefixes (see above); mark fields 10–12 as not provided | | User provides a file **without a header row** and extra columns are present | Warn: only emails were imported; other columns were discarded. Ask user to add a header row (`email`, `company`, `name`, etc.) and re-upload | --- ### Carry Forward Store all answers as session context. **Every subsequent step uses this context directly — never ask for the same information again.** ``` session_context = { sender_company: "...", # or null sender_name: "...", # or null sender_title: "...", # or null sender_website: "...", # or null sender_offerings: "...", # or null sender_proof: "...", # or null recipients: [ # each entry always has a name (user-provided or auto-derived) { email: "...", name: "...", company: "...", title: "...", domain: "..." }, ... ], campaign_purpose: "...", recipient_fields: { name: true, # ALWAYS true — either user-provided or auto-derived from email prefix company: true/false, title: true/false, domain: true/false, } } ``` --- ## Step 1 — Verify Recipient Emails **Always verify before sending to >10 recipients.** ### Use the bundled script (MANDATORY) ```bash source ~/.zshrc # Option A: inline emails python3 scripts/verify_emails.py [REDACTED_SECRET] [REDACTED_SECRET] # Option B: from file (TXT/CSV/JSON) python3 scripts/verify_emails.py --file leads.csv # Option C: override column detection python3 scripts/verify_emails.py --file leads.csv --email-column "Email Address" ``` **Script flags:** | Flag | Default | Description | |------|---------|-------------| | `emails` | — | Space-separated email addresses | | `--file` | — | Read from TXT/CSV/JSON file | | `--email-column` | auto-detect | Override email column name | | `--timeout` | 180 | Poll timeout in seconds | | `--interval` | 5 | Poll interval in seconds | **Output:** JSON on stdout with categorized results: ```json { "total": 5, "valid": [{"email": "...", "status": "VALID"}], "invalid": [...], "domain_error": [...], "unknown": [...], "unchecked": [...] } ``` ### Email Status Values | Status | Meaning | Recommendation | |--------|---------|----------------| | `VALID` | Confirmed deliverable | Send | | `UNKNOWN` | Catch-all or greylisting | Send with caution | | `DOMAIN_ERROR` | No MX records | Do not send | | `INVALID` | Malformed or SMTP-rejected | Do not send | | `UNCHECKED` | Still processing | Wait or re-poll | **Filtering guidance:** Conservative = `VALID` only. Aggressive = `VALID` + `UNKNOWN`. ### Behavior Rules - Never report results while `UNCHECKED` entries remain (unless timeout) - Show progress during polls: "Verifying... X/total processed" - Ask before filtering: "Found N undeliverable addresses. Remove them?" - Credit error = stop immediately - UNKNOWN is not a hard block — explain the tradeoff ### API Details (edge cases only) - **Submit:** `POST /sirius/api/biz/email/verification` — body: `{"emails": [...]}` - **Poll:** `GET /sirius/api/biz/email/verification/task/{taskId}` - Max 10,000 emails per task; script auto-batches larger lists - 120 req/min rate limit; on 429 wait 60s --- ## Step 2 — Claim Free Pre-Warmed Mailbox **Run before creating any campaign.** Each user gets one free mailbox (60 days, 90+ sender score). ### Use the bundled script (MANDATORY) ```bash source ~/.zshrc # Claim (idempotent — safe to run multiple times) python3 scripts/claim_free_mailbox.py # JSON output for scripting python3 scripts/claim_free_mailbox.py --json ``` **What you get:** - Pre-warmed mailbox with 90+ sender score - 60-day validity, no credit card required - Idempotent: re-running returns the same mailbox - stdout outputs just the email address (for piping) > **Note:** The free mailbox uses a **randomly assigned domain** (e.g. `[REDACTED_SECRET]`) which may not match your company domain. If brand consistency matters, consider purchasing a dedicated domain at https://app.mailgo.ai. ### Auto-integration After claiming, use the returned email as `--sender` in Step 4: ```bash SENDER=$(python3 scripts/claim_free_mailbox.py) python3 scripts/run_campaign.py --sender "$SENDER" ... ``` ### API Details (edge cases only) - **Endpoint:** `POST /api/biz/benefits/assign-prewarm` - **Auth:** `X-API-Key: {key}` - **Response:** `{"code": 0, "data": "[REDACTED_SECRET]"}` - `data: null` = pool empty, contact support --- ## Step 3 — Generate & Optimize Email Content **This is a knowledge-and-rewrite step. No API calls. No scripts.** Apply the 6-step optimization pipeline to user content, or generate from scratch. ### Prerequisites for Generation — Use Session Context (CRITICAL) All sender and recipient information was collected in **Step 0.5**. Do NOT ask again. Read directly from `session_context`: | Context field | Use in email | |---------------|--------------| | `sender_name` | Signature name | | `sender_title` | Signature title (omit if null) | | `sender_company` | Signature company + body references | | `sender_website` | Signature website (omit if null) | | `sender_offerings` | Value bridge paragraph | | `sender_proof` | Proof point sentence (omit if null) | | `recipient_fields.name` | **Always `true`** — use `#{Name}` unconditionally (user-provided or auto-derived from email prefix in Step 0.5) | | `recipient_fields.company` | Use `#{Company Name}` only if `true` | | `recipient_fields.title` | Use `#{Title}` only if `true` | | `recipient_fields.domain` | Use `#{Domain}` only if `true` | > If `session_context` is missing (e.g. user jumped directly to Step 3), collect the missing fields inline — but only the ones not yet known. ### Template Variables and Placeholder Rules (CRITICAL) Mailgo uses `#{...}` placeholders that are resolved server-side at send time: | Placeholder | Description | Resolved from | |-------------|-------------|---------------| | `#{Name}` | Recipient's name | `contactName` in leads data | | `#{Company Name}` | Company name | `companyName` in leads data | | `#{Domain}` | Company website | `domain` in leads data | | `#{Title}` | Job title | `title` in leads data | | `#{Email}` | Email address | `contactEmail` in leads data | **CRITICAL — Conditional Placeholder Rule:** - **ONLY use a `#{...}` placeholder if the corresponding data is confirmed available** (either from the file columns or user-provided inline data). - **If a field has NO data, NEVER use its `#{...}` placeholder.** The placeholder will resolve to empty string at send time, creating ugly output like `，您好` or `关于 的合作`. - **Exception: `#{Name}` is ALWAYS available.** Every recipient has a name — either provided by the user or auto-derived from the email prefix in Step 0.5. Never fall back to `"您好"` when you have email addresses. - For other fields, use a generic alternative: | Placeholder | If data available | If data NOT available | |-------------|-------------------|----------------------| | `#{Name}` | `#{Name}，您好` | _(never happens — always use `#{Name}`)_ | | `#{Company Name}` | `关于 #{Company Name} 的合作` | `关于贵公司的合作` / `About a potential partnership` | | `#{Domain}` | `我浏览了 #{Domain}` | Omit this sentence entirely | | `#{Title}` | `作为 #{Title}` | Omit or use generic phrasing | **Log placeholder decisions** in the change summary: ``` Placeholders used: #{Name} (data available), #{Email} (always available) Placeholders skipped: #{Company Name} (no data), #{Title} (no data), #{Domain} (no data) ``` ### Optimization Pipeline (execute ALL 6 steps, in order) **Step 3.1 — Normalize** - Plain text input → wrap in HTML skeleton (see below) - HTML input → preserve structure **Step 3.2 — Spam Trigger Scan** - Scan subject + body against `resources/spam-triggers.md` - Replace each trigger with neutral alternative - Log every replacement Key replacements (most common): | Trigger | Replacement | |---------|-------------| | `free` / `free trial` | `included` / `trial period` | | `Click here` | `Learn more` / `See details` | | `Act now` | `When you're ready` | | `Limited time` | `While available` | | `Guaranteed` | `Proven` | | `Dear friend` | `Hi #{Name}` | | ALL CAPS words | Sentence case | | `!!!` / `???` | Single punctuation | Full replacement table: `resources/spam-triggers.md` **Step 3.3 — HTML Cleanup** - Remove: `<script>`, `<style>`, `<meta>`, `<link>`, `<iframe>`, `<embed>`, `<object>` - Remove: event handlers (`onclick`, `onload`, etc.) - Convert `<style>` blocks → inline styles, then remove block - Remove: HTML comments, CSS `!important`, base64 images - Ensure: all `<img>` have `alt` attributes **Step 3.4 — Structure Check** - Text-to-image ratio > 60% text (warn if image-heavy) - Cold email target: < 150 words (flag if > 200) - One CTA only (suggest consolidation if multiple) - **Placeholder Data Validation** — scan all `#{...}` in the content against `session_context.recipient_fields`: - `#{Name}` and `#{Email}` → always safe, never touch - `#{Company Name}` → if `recipient_fields.company == false`: replace with generic text (e.g. `贵公司` / `your company`); log the replacement - `#{Domain}` → if `recipient_fields.domain == false`: remove the sentence containing it entirely; log the removal - `#{Title}` → if `recipient_fields.title == false`: replace with generic phrasing or omit; log the replacement - If data IS available for a placeholder: preserve it unchanged **Step 3.5 — Link Audit** - Replace URL shorteners (bit.ly, t.co, tinyurl, goo.gl, ow.ly) with full URLs - Warn if > 3 links in cold email - Remove `javascript:` and `data:` URLs **Step 3.6 — Final Polish** - Add soft opt-out line if missing (see below) - Verify HTML is well-formed - Write to `/tmp/email_optimized_<timestamp>.html` ### Content Generation (from scratch) Generate **4 subject line variants** for A/B testing. Adapt based on available data: **If `#{Company Name}` data IS available:** | # | Strategy | Example | |---|----------|---------| | 1 | Value proposition | `How #{Company Name} can cut onboarding time by 60%` | | 2 | Industry insight | `A shift I'm seeing in #{Company Name}'s space` | | 3 | Case study / metrics | `How [similar company] grew pipeline 3x in 90 days` | | 4 | Curiosity / question | `Quick question about #{Company Name}'s [process]` | **If `#{Company Name}` data is NOT available:** | # | Strategy | Example | |---|----------|---------| | 1 | Value proposition | `Cutting onboarding time by 60% — here's how` | | 2 | Industry insight | `A shift I'm seeing in the [industry] space` | | 3 | Case study / metrics | `How [similar company] grew pipeline 3x in 90 days` | | 4 | Curiosity / question | `A quick question about your [process]` | **Subject rules:** 40-50 chars, always personalize with `#{Name}` (always available); use `#{Company Name}` only if data available. No ALL CAPS, no spam triggers, no fake Re:/Fwd:. **Body structure (adapt based on available fields):** ``` 1. Personal opener (1 sentence) - ALWAYS use "#{Name}，您好：" — #{Name} is always available (user-provided or auto-derived) - WITH #{Company Name}: reference it in opener - WITHOUT #{Company Name}: use generic "贵公司" / "your company" or omit 2. Value bridge (1-2 sentences) — connect their situation to your offering 3. Proof point (1 sentence) — specific metric or case study (use sender_proof if available) 4. Soft CTA (1 sentence) — question, not demand 5. Signature — use actual sender_name + sender_title + sender_company (NEVER use placeholders like [Name]) 6. Soft opt-out ``` **Body rules:** under 150 words, conversational tone, one CTA. All sender info in signature must be real data, not placeholders. **Select industry template** from `resources/industry-templates.md` based on user's offerings. ### HTML Skeleton Use for plain-text wrapping or building from scratch: ```html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> </head> <body style="margin:0; padding:20px; font-family:Arial, sans-serif; font-size:14px; line-height:1.6; color:#333333;"> <div style="max-width:600px; margin:0 auto;"> <!-- EMAIL CONTENT HERE --> </div> </body> </html> ``` ### Soft Opt-Out Line Always add after signature if missing: ``` P.S. If this isn't relevant, just reply and let me know — I won't reach out again. ``` ### Change Summary Format ``` Email Optimization Complete --- Industry template: SaaS / Software (matched from "AI deployment platform") Sender info: Zhang Wei, Sales Director, TirePro Technologies (www.tirepro.com) Recipient data available: name (auto-derived from email prefix), company (no), title (no), domain (no) Placeholders used: #{Name} (auto-derived from email prefix — e.g. [REDACTED_SECRET] → "Alice Smith") Placeholders skipped: #{Company Name}, #{Title}, #{Domain} (no data — generic wording used) Spam triggers replaced: 3 - "free trial" → "trial period" - "Click here" → "Learn more" - "Act now" → "When you're ready" HTML issues fixed: 1 - Removed <script> tag Structure: 118 words (OK) Links: 2 (OK) Opt-out: Added Subjects generated: 4 variants Output: /tmp/email_optimized_1711468800.html → Use with: --body-file /tmp/email_optimized_1711468800.html ``` ### Severity Levels | Level | Issues | |-------|--------| | **Critical** (must fix) | `<script>` tags, shortener URLs, base64 images, `javascript:` URLs, "This is not spam", `#{Company Name}`/`#{Domain}`/`#{Title}` placeholders with no corresponding recipient data | | **Warning** (recommend) | Spam trigger words, image-heavy, >3 links, missing opt-out | | **Info** | Word count >150, multi-punctuation, subject line suggestions | ### Behavior Rules - Run automatically before every campaign send, even if not asked. Report "0 issues found" if clean. - If user says "don't change my content": scan and **report** only, do not modify. - Never change meaning. Replace words, don't rewrite paragraphs. - **Placeholder rule differs by scenario:** - **Generating content from scratch:** NEVER write a `#{...}` placeholder unless the corresponding `recipient_fields` value is `true`. Use the generic alternatives from the Placeholder Rules table instead. - **Optimizing existing content:** if the content already contains a `#{...}` placeholder with NO data available, MUST replace it with the generic alternative (Step 3.4 Placeholder Data Validation). Do not silently preserve it — an unresolved placeholder will appear as a blank gap to the recipient. - **Exception: `#{Name}` and `#{Email}` are always safe** — never remove or replace them. - **NEVER use `[bracketed placeholders]` like `[Name]`, `[Company]`, `[Title]` in final email output.** These must be replaced with actual sender data or omitted. They are template authoring aids, not send-time variables. - Always use actual sender info (name, title, company, website) in the email signature — collected from the user in the Prerequisites step. - Write HTML to `/tmp/email_optimized_<timestamp>.html` and use via `--body-file`. --- ## Step 4 — Create & Send Campaign ### Use the bundled script (MANDATORY) ```bash source ~/.zshrc # Option A: inline recipients python3 scripts/run_campaign.py \ --sender "[REDACTED_SECRET]" \ --subject "Quick question about #{Company Name}" \ --body-file /tmp/email_optimized_1711468800.html \ --recipients "[REDACTED_SECRET],[REDACTED_SECRET]" \ --campaign-name "SaaS Outreach Mar 2026" \ --daily-limit 50 # Option B: recipients from file (CSV/XLSX/TXT/JSON) python3 scripts/run_campaign.py \ --sender "[REDACTED_SECRET]" \ --subject "Quick question about #{Company Name}" \ --body-file /tmp/email_optimized_1711468800.html \ --recipients-file leads.xlsx \ --campaign-name "SaaS Outreach Mar 2026" ``` **Script flags:** | Flag | Default | Description | |------|---------|-------------| | `--sender` | required | Sender email (from Step 2) | | `--subject` | required | Subject line | | `--body` | — | HTML body (or use `--body-file`) | | `--body-file` | — | Read body from file (from Step 3) | | `--recipients` | — | Comma-separated emails | | `--recipients-file` | — | CSV/XLSX/TXT/JSON file | | `--email-column` | auto | Override email column name | | `--name-column` | auto | Override name column name | | `--company-column` | auto | Override company column name | | `--title-column` | auto | Override title/job column name | | `--domain-column` | auto | Override domain/website column name | | `--recipient-names` | auto from email | Comma-separated display names | | `--recipient-companies` | — | Comma-separated company names | | `--recipient-titles` | — | Comma-separated job titles | | `--recipient-domains` | — | Comma-separated company websites | | `--campaign-name` | "Campaign" | Display name | | `--timezone-id` | Asia/Singapore | Timezone | | `--timezone-offset` | +08:00 | UTC offset | | `--send-days` | 1,2,3,4,5 | Days (1=Mon...7=Sun) | | `--send-hours` | 9,18 | Start,end hour | | `--daily-limit` | 50 | Daily send limit | | `--no-tracking` | off | Disable open/click tracking | | `--dry-run` | off | Create but do not activate | **Output:** JSON summary with `campaignId`, `emailContentId`, lead counts, activation status. ### Agent Decision Framework All campaign inputs were collected in **Step 0.5**. Use `session_context` directly. **From session_context (never ask again):** - Recipients — `session_context.recipients` - Purpose/goal — `session_context.campaign_purpose` - Sender info — `sender_company`, `sender_name`, `sender_title`, `sender_website`, `sender_offerings`, `sender_proof` - Recipient field availability — `recipient_fields.{name, company, title, domain}` **Auto-inferred (never ask):** - Sender email — auto-assigned from Step 2 (free mailbox) - Subject — generate from `campaign_purpose` (Step 3), using only placeholders with confirmed `recipient_fields` - Body — generate personalized HTML (Step 3), respecting `recipient_fields` availability - Campaign name — derived from `campaign_purpose` + current month (e.g. "Tire Outreach Mar 2026") - Schedule — Mon-Fri 9am-6pm, Asia/Singapore timezone - Daily limit — 50 for new accounts - Tracking — open + click ON ### Decision Flow ``` 0.5. [Already done] All info collected upfront — use session_context throughout 1. Verify recipients (Step 1) if > 10 emails 2. Claim mailbox (Step 2) if no sender specified 3. Generate & optimize content (Step 3) — use ONLY placeholders with confirmed recipient_fields 4. Run scripts/run_campaign.py (Step 4) — pass all available recipient fields 5. Report: campaignId, name, lead count, activation status ``` ### API Reference (edge cases only) Campaign creation is a **4-step sequential flow** handled by the script: ``` Step 4a — Upload content: POST /campaign/api/biz/mailgo/email/content/upload Step 4b — Create campaign: POST /campaign/api/biz/mailgo/campaign/save Step 4c — Add leads: POST /campaign/api/biz/mailgo/leads/add Step 4d — Activate: POST /campaign/api/biz/mailgo/campaign/operate ``` Fall back to raw API only for: updating existing campaigns or adding leads from contact groups. --- ## Step 5 — Manage Campaign Lifecycle ### Use the bundled script (MANDATORY) ```bash source ~/.zshrc # Activate / Pause / Delete python3 scripts/campaign_control.py activate <campaignId> python3 scripts/campaign_control.py pause <campaignId> python3 scripts/campaign_control.py delete <campaignId> # confirm with user first! # List campaigns python3 scripts/campaign_control.py list # all python3 scripts/campaign_control.py list --status 1 # active python3 scripts/campaign_control.py list --status 0 # paused python3 scripts/campaign_control.py list --name "outreach" # fuzzy search # Get campaign detail python3 scripts/campaign_control.py info <campaignId> ``` ### Decision Flow ``` User says "stop/pause/resume/delete <campaign>" 1. Know campaign ID? YES → run script directly NO → run: scripts/campaign_control.py list --status 1,0 show results, ask user to pick one 2. If delete → confirm: "Delete campaign '{name}'? This cannot be undone." 3. Run script, report: "Campaign '{name}' (ID: {id}) is now {STATUS}." ``` ### Campaign Status Reference | Code | Name | Description | |------|------|-------------| | `-1` | DRAFT | Not activated | | `0` | PAUSE | Paused | | `1` | ACTIVE | Sending | | `2` | COMPLETE | All leads processed | | `3` | ERROR | Error state | State transitions: DRAFT->ACTIVE, ACTIVE<->PAUSE, any->deleted ### Abnormal States (`abnormalType`) | Code | Meaning | Action | |------|---------|--------| | `1` | Monthly quota exhausted | Upgrade plan | | `2` | Some sender accounts have issues | Check account settings | | `4` | All sender accounts have issues | Fix sender accounts first | | `9` | Daily sending limit reached | Wait for next day | | `11` | Email quota exhausted | Upgrade plan | --- ## Step 6 — View Campaign Reports ### Use the bundled script (MANDATORY) ```bash source ~/.zshrc # Overview stats python3 scripts/campaign_report.py overview <campaignId> # Per-round breakdown python3 scripts/campaign_report.py rounds <campaignId> # Daily progress (requires date range) python3 scripts/campaign_report.py daily <campaignId> --start 2026-03-25 --end 2026-03-31 # List replies (metadata only) python3 scripts/campaign_report.py replies <campaignId> # List replies + read plain-text body of each reply python3 scripts/campaign_report.py replies <campaignId> \ --sender "[REDACTED_SECRET]" \ --read-content # Raw JSON output python3 scripts/campaign_report.py --json overview <campaignId> ``` ### Output Format Always show rates, not raw numbers alone: ``` Campaign: "Q1 Outreach" (ID: 123456) Status: ACTIVE Leads: 500 total | 250 completed | 30 in progress | 200 not yet | 20 failed Sent: 450 Delivered: 420 (93.3%) Opened: 180 (42.9%) Replied: 35 ( 8.3%) Clicked: 65 (15.5%) Bounced: 30 ( 6.7%) ``` ### Deliverability Health Metrics | Metric | Healthy | Critical | |--------|---------|----------| | Open rate | >20% | <10% | | Bounce rate | <2% | >5% | | Spam complaints | <0.1% | >0.3% | ### API Reference (for advanced queries) | Report | Endpoint | |--------|----------| | Overview | `POST /campaign/api/biz/mailgo/stat/overview` | | Per-round | `POST /campaign/api/biz/mailgo/stat/sequence-info` | | Send records | `POST /campaign/api/biz/mailgo/stat/sequence-detail` | | Reply list | `POST /campaign/api/biz/mailgo/stat/sequence-detail-reply` | | Reply content | `POST /tp/api/hmail/proxy` — `func: mbox:readMessage`, `mid: <messageId>` | | Reply detail | `POST /campaign/api/biz/mailgo/stat/sequence-detail-reply` | | Click detail | `POST /campaign/api/biz/mailgo/stat/sequence-detail-click` | | Contact history | `POST /campaign/api/biz/mailgo/stat/leads-send` | | Daily progress | `POST /campaign/api/biz/mailgo/stat/daily-step` | ### Behavior Rules - Default to overview. Drill into details only on explicit request. - Always show rates alongside raw counts. - If `sent == 0`: say "No emails sent yet" — do not divide by zero. - If no campaign ID: list campaigns first, offer to drill into one. - If `openClick == 0`: tracking was disabled — say so. - **If `replied > 0` in overview:** proactively suggest reading replies: ``` 💬 X reply(ies) detected. Run the following to read them: python3 scripts/campaign_report.py replies <campaignId> \ --sender <senderEmail> --read-content ``` --- ## Global Error Handling | HTTP | Action | |------|--------| | `401` | Re-run Step 0 (token expired or invalid) | | `403` | Ensure `User-Agent` header is set | | `429` | Wait 60s, retry | | `500` | Retry after 10s | | `502/503` | Retry after 30-60s | --- ## Global Behavior Rules 1. **Script-first:** Always use bundled scripts. Never reimplement API calls inline. 2. **Verify before send:** Always run Step 1 for campaigns with >10 recipients. 3. **Optimize before send:** Always run Step 3 before sending, even if not explicitly asked. Report "0 issues" if clean. 4. **Confirm destructive actions:** Always confirm before delete. Campaign deletion is irreversible via API. 5. **Security:** Never handle API keys in chat. User must create token in Personal Tokens and set as environment variable. 6. **Zero dependencies:** All scripts use Python stdlib only (`urllib`, `json`, `csv`, `ssl`). No `pip install` required (except optional `openpyxl` for .xlsx files).


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)