From morning-ai
Sends daily MorningAI AI news digest as multipart HTML email via SMTP to configured recipients from scored JSON data. Supports dry-run, attachments, and rate limiting.
npx claudepluginhub octo-patch/morningai --plugin morning-aiThis skill uses the workspace's default tool permissions.
Distribute the daily AI news digest as **multipart HTML email** to a configured recipient list via SMTP. Reads the same `data_{DATE}.json` as `gen-message` (already scored, deduped, cross-source verified) and produces:
Generates concise Markdown text digest and 9:16 PNG image from scored AI news data for sharing on messaging platforms like WeChat, Telegram, Slack.
Guides email system design with transactional/marketing separation, deliverability (SPF/DKIM/DMARC), queuing with retries, event tracking (opens/bounces), and best practices. Useful for notifications and campaigns.
Handles Resend email API: send single/batch transactional emails, inbound webhooks, manage templates/domains/contacts/broadcasts/events/logs/API keys/automations, setup SDK with gotchas like idempotency keys.
Share bugs, ideas, or general feedback.
Distribute the daily AI news digest as multipart HTML email to a configured recipient list via SMTP. Reads the same data_{DATE}.json as gen-message (already scored, deduped, cross-source verified) and produces:
email_{DATE}.html (also used as the email body)email_{DATE}.txt (multipart/alternative pairing)email_{DATE}_manifest.json (per-recipient send status)This is the first true automated push channel in the pipeline — gen-message / gen-social only generate files for manual sharing, while gen-email actually delivers to inboxes.
Factual integrity inherited from gen-message: every item must have a source_url. Items with score ≥ 7 must additionally satisfy verified == true. Drop items lacking a credible source.
| Variable | Required | Default | Description |
|---|---|---|---|
EMAIL_ENABLED | — | false | Master switch — when unset/false, the step is silently skipped |
EMAIL_RECIPIENTS | ✓ | — | Comma-separated list, e.g. alice@x.com,bob@y.com |
EMAIL_RECIPIENTS_FILE | — | .claude/recipients.json | Optional JSON file — overrides env list when present |
EMAIL_SMTP_HOST | ✓ | — | SMTP server, e.g. smtp.gmail.com, smtp.qq.com |
EMAIL_SMTP_PORT | — | 587 | Port (587=STARTTLS, 465=SSL, 25=plain) |
EMAIL_SMTP_TLS | — | starttls | starttls / ssl / none |
EMAIL_SMTP_USER | ✓ | — | SMTP auth username |
EMAIL_SMTP_PASSWORD | ✓ | — | SMTP password (Gmail: use App Password, not account password) |
EMAIL_FROM | — | EMAIL_SMTP_USER | Display sender, e.g. MorningAI <noreply@x.com> |
EMAIL_REPLY_TO | — | — | Reply-To header |
EMAIL_SUBJECT_TEMPLATE | — | MorningAI {date} · {n} updates | Subject template, supports {date} {n} {lang} |
EMAIL_LANG | — | follows --lang | Email language: zh / en / ja |
EMAIL_MIN_SCORE | — | 5 | Min importance score to include |
EMAIL_MAX_ITEMS | — | 10 | Max items in the digest |
EMAIL_ATTACH_IMAGE | — | true | Attach message_{DATE}.png if it exists |
EMAIL_RATE_LIMIT_DELAY | — | 1 | Seconds to sleep between recipients (anti-throttling) |
EMAIL_LIST_UNSUBSCRIBE | — | — | Unsubscribe target, e.g. mailto:admin@x.com?subject=Unsubscribe |
EMAIL_DRY_RUN | — | false | If true, generate email_*.html/email_*.txt locally and do not send |
EMAIL_KOL_ENABLED | — | true | Render a separate "KOL Voices" section after the main items. Set false to suppress entirely |
EMAIL_KOL_MIN_SCORE | — | 4 | Min importance for KOL items (independent of EMAIL_MIN_SCORE because KOL voices are scored 4-7 by design) |
EMAIL_KOL_MAX_ITEMS | — | 5 | Max KOL items to include in the section |
EMAIL_KOL_SHOW_EMPTY | — | true | When no KOL items qualify, still render the section header with a "today was quiet" empty-state line. Set false to omit the section on quiet days |
EMAIL_RECIPIENTS_FILE)[
{"email": "alice@example.com", "name": "Alice", "lang": "zh", "min_score": 6, "active": true},
{"email": "bob@example.com"},
{"email": "paused@example.com", "active": false}
]
Per-recipient lang and min_score override the global defaults. Inactive entries are skipped.
Same as gen-message (see skills/gen-message/SKILL.md for full spec):
data_{DATE}.json from CWD.importance >= min_score (per-recipient or global), source_url present, verified == true for score ≥ 7.is_kol_voice == true — they render in the dedicated KOL section, not mixed into the main feed.product ≤ 4, model ≤ 3, benchmark ≤ 2, financing ≤ 2; overflow filled by score.importance desc, cap at EMAIL_MAX_ITEMS.A separate block rendered after the main items, mirroring the ## KOL Voices section in report_{DATE}.md and the dedicated KOL channel in gen-social. Independent commentary deserves its own visual lane — without it, KOL takes either get suppressed by the 7+ verification gate (KOL voices are scored 4-7 by design) or appear interleaved with vendor announcements.
KOL filter rules:
is_kol_voice == true.importance >= EMAIL_KOL_MIN_SCORE (default 4, lower than the main EMAIL_MIN_SCORE).source_url.gen-infographic's KOL handling: KOL voices are commentary, not vendor announcements that need cross-source verification.importance desc, capped at EMAIL_KOL_MAX_ITEMS.When zero KOL items qualify, the section is still rendered with a "today's KOL voices were quiet" line if EMAIL_KOL_SHOW_EMPTY=true (the default), so subscribers see the section is alive even on quiet days. Set EMAIL_KOL_SHOW_EMPTY=false to omit the section entirely on quiet days, or EMAIL_KOL_ENABLED=false to suppress it permanently.
The send_email.py script handles all of the following automatically:
EMAIL_ENABLED != true.EMAIL_SMTP_HOST / EMAIL_SMTP_USER / EMAIL_SMTP_PASSWORD / recipients) — exit with clear error if missing.data_{DATE}.json and apply selection rules.EMAIL_RECIPIENTS_FILE (if exists) or EMAIL_RECIPIENTS.EMAIL_LANG → write email_{DATE}.html and email_{DATE}.txt.EMAIL_DRY_RUN=true, exit after writing previews (no SMTP traffic).EMAIL_RATE_LIMIT_DELAY seconds between sends.email_{DATE}_manifest.json with per-recipient status (sent / failed) and error text.The orchestrator (skills/morning-ai/SKILL.md Step 7) runs:
python3 skills/gen-email/scripts/send_email.py --date {YYYY-MM-DD}
Optional flags:
--date YYYY-MM-DD — override the date (default: today UTC+8)--data PATH — override the data file path (default: data_{DATE}.json in CWD)--lang zh|en|ja — override EMAIL_LANG--dry-run — equivalent to EMAIL_DRY_RUN=trueEMAIL_LIST_UNSUBSCRIBE is set. Major clients (Gmail, Outlook, Apple Mail) display a one-click unsubscribe button.EMAIL_RATE_LIMIT_DELAY) prevents tripping SMTP provider throttles (Gmail caps free accounts at ~100 sends/day, with short bursts blocked).When a recipient asks to unsubscribe, the admin manually removes them from EMAIL_RECIPIENTS or recipients.json. Self-service unsubscribe requires a hosted backend (out of scope for this skill).
See docs/email-setup.md for full details. Quick reference:
| Provider | Host | Port | TLS | Notes |
|---|---|---|---|---|
| Gmail | smtp.gmail.com | 587 | STARTTLS | Requires App Password (turn on 2FA → generate App Password) |
| QQ Mail | smtp.qq.com | 465 | SSL | Use authorization code, not login password |
| Outlook | smtp.office365.com | 587 | STARTTLS | Account password or App Password |
| Alibaba Cloud Enterprise Mail | smtp.qiye.aliyun.com | 465 | SSL | Account password |
{
"date": "2026-04-20",
"subject": "MorningAI 2026-04-20 · 8 updates",
"total": 3,
"succeeded": 2,
"failed": 1,
"results": [
{"email": "alice@example.com", "status": "sent", "message_id": "<...@morning-ai>"},
{"email": "bob@example.com", "status": "sent", "message_id": "<...@morning-ai>"},
{"email": "carol@example.com", "status": "failed", "error": "smtp error: 550 mailbox unavailable"}
]
}
The manifest enables retry logic in future runs (skip already-sent recipients) and makes failures visible without scraping logs.