Skip to content
Back to skills

Gen Email

ASecurity

Send the daily MorningAI digest as HTML email via SMTP to a configured recipient list.

  • 1,531 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
ai-agentspythongobashapibackend

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 4 files and shows the line behind each finding

Scanned November 7, 2026

npx -y skills add hashgraph-online/awesome-codex-plugins --skill gen-email --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Gen Email?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Gen Email
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/hashgraph-online-gen-email/badge)](https://www.skillsdirectory.com/skills/hashgraph-online-gen-email)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: gen-email
version: "1.4.0"
description: Send the daily MorningAI digest as HTML email via SMTP to a configured recipient list.
---

## Objective

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:

1. **HTML email body** — `email_{DATE}.html` (also used as the email body)
2. **Plain text fallback** — `email_{DATE}.txt` (multipart/alternative pairing)
3. **Send manifest** — `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.

---

## Configuration

| 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 |

### Recipients JSON format (`EMAIL_RECIPIENTS_FILE`)

```json
[
  {"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.

---

## Content Selection Rules

Same as `gen-message` (see `skills/gen-message/SKILL.md` for full spec):

1. Load `data_{DATE}.json` from CWD.
2. Filter: `importance >= min_score` (per-recipient or global), `source_url` present, `verified == true` for score ≥ 7.
3. **Skip items with `is_kol_voice == true`** — they render in the dedicated KOL section, not mixed into the main feed.
4. Apply category balance: `product` ≤ 4, `model` ≤ 3, `benchmark` ≤ 2, `financing` ≤ 2; overflow filled by score.
5. Sort by `importance` desc, cap at `EMAIL_MAX_ITEMS`.

### KOL Voices Section

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:
1. Only items with `is_kol_voice == true`.
2. `importance >= EMAIL_KOL_MIN_SCORE` (default `4`, lower than the main `EMAIL_MIN_SCORE`).
3. Must have `source_url`.
4. **No 7+ verification gate** — matches `gen-infographic`'s KOL handling: KOL voices are commentary, not vendor announcements that need cross-source verification.
5. Sorted by `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.

---

## Workflow Summary

The `send_email.py` script handles all of the following automatically:

1. Read config, exit silently if `EMAIL_ENABLED != true`.
2. Validate required config (`EMAIL_SMTP_HOST` / `EMAIL_SMTP_USER` / `EMAIL_SMTP_PASSWORD` / recipients) — exit with clear error if missing.
3. Load `data_{DATE}.json` and apply selection rules.
4. Load recipients from `EMAIL_RECIPIENTS_FILE` (if exists) or `EMAIL_RECIPIENTS`.
5. Render preview using global `EMAIL_LANG` → write `email_{DATE}.html` and `email_{DATE}.txt`.
6. If `EMAIL_DRY_RUN=true`, exit after writing previews (no SMTP traffic).
7. Otherwise send to each recipient (re-render in their preferred language), sleeping `EMAIL_RATE_LIMIT_DELAY` seconds between sends.
8. Write `email_{DATE}_manifest.json` with per-recipient `status` (`sent` / `failed`) and `error` text.

### Invocation

The orchestrator (`skills/morning-ai/SKILL.md` Step 7) runs:

```bash
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=true`

---

## Compliance & Anti-Spam

- **List-Unsubscribe header (RFC 2369)** is automatically added when `EMAIL_LIST_UNSUBSCRIBE` is set. Major clients (Gmail, Outlook, Apple Mail) display a one-click unsubscribe button.
- **Plain-text fallback** is always included (multipart/alternative). Improves deliverability for clients that strip HTML.
- **Rate limiting** (`EMAIL_RATE_LIMIT_DELAY`) prevents tripping SMTP provider throttles (Gmail caps free accounts at ~100 sends/day, with short bursts blocked).
- **Per-recipient send** (no BCC) — protects subscriber privacy, allows per-recipient personalization (language), and avoids "looks like spam blast" heuristics.

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).

---

## Common SMTP Configurations

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 |

---

## Example: Manifest Output

```json
{
  "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.

Files in this skill

  • SKILL.md8.4 KB
  • scripts/send_email.py12.1 KB
  • templates/README.md1.3 KB
  • templates/subject_examples.md1.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.