email-sender
The post-write distribution hook. Every vault write worth surfacing beyond the local Obsidian copy goes through here. Scheduled digests fire-and-forget to the distribution list; ad-hoc research notes prompt the user [y/n] before sending. The vault note remains the canonical record; email is a delivery channel, not a replacement.
When to use
- Immediately after
vault-writer.write_digest() succeeds — invoked from scheduled-agent-runner step 11.
- Immediately after
vault-writer.write_research() succeeds in any Category 1 researcher — researcher invokes prompt_then_send.
- User explicitly asks: "email this", "send the latest weekly digest to my distribution list", "show me my distribution list".
When NOT to use
- Writes to
facts/, events/, decisions/, insights/, people/, projects/, _inbox/ — durable knowledge, not distribution targets. (auto_send no-ops on these surfaces.)
- Non-interactive context AND surface is
research — prompt_then_send requires a user; degrade gracefully by skipping.
- Mid-stream (vault write hasn't completed yet — wait for the write).
Prerequisites
Two pieces of config:
Gmail credentials at ~/.config/research-bot/env:
GMAIL_SEND_ADDRESS=you@gmail.com
GMAIL_APP_PASSWORD=xxxxxxxxxxxxxxxx
Requires 2-Step Verification enabled on the account. Generate at https://myaccount.google.com/apppasswords ("Mail" → "Other / research-bot"). Use the helper script: scripts/set-gmail-credentials.sh "you@gmail.com" "xxxx xxxx xxxx xxxx" (note the leading space to keep the command out of shell history).
Distribution list at vault/_config/email-distribution.md. Copy email-distribution.example.md to that path on first use and edit.
If either is missing, the skill stops and reports — never silently drops.
Helpers
send_note(note_path, subject_override=None)
The primary action. Steps:
- Resolve
note_path to an absolute path; confirm the file exists.
- Load and parse
vault/_config/email-distribution.md (see Parsing).
- Confirm
~/.config/research-bot/env carries GMAIL_SEND_ADDRESS and GMAIL_APP_PASSWORD (the script re-reads them, but check here so a missing credential surfaces as a clean stop-and-report referencing the setup helper).
- Derive the Subject:
subject_override if given, else derive from the note (see Subject derivation).
- Per-recipient validation: drop any address that doesn't match
^[^\s@]+@[^\s@]+\.[^\s@]+$ (record it for the skipped list). A single bad entry doesn't lose the rest.
- Invoke
render_and_send.py (in this skill folder), piping a JSON payload on stdin:{"note_path": "<abs path>", "subject": "<derived subject>",
"bcc": ["<validated recipient>", "…"],
"vault_footer_path": "digests/weekly/2026-…md"}
The script builds the message and sends it: From/To = GMAIL_SEND_ADDRESS (self), Bcc = the validated recipients (addresses are NOT disclosed to each other — send_message strips the Bcc header before transmit). It renders the note's Markdown (frontmatter stripped) to styled HTML, attaches the raw .md, and emits a JSON result on stdout (or an error/error_type object + non-zero exit on failure — map error_type to the matching stop-and-report case below). See Message body shape.
Return:
{
"sent_to": ["addr1@example.com", "addr2@example.com"], # from the script's result
"skipped": [{"email": "bad@@invalid", "reason": "invalid format"}], # from step 5
"subject": "[Weekly Intelligence Digest] 2026-06-22 — ...",
"from": "you@gmail.com",
"html": true, # false → markdown lib absent, plain-text-only fallback
"attached": "2026-06-22-weekly-intelligence-digest.md"
}
prompt_then_send(note_path)
For research notes. Asks the user, then calls send_note or skips.
- Load + parse the distribution list (so the prompt can show the recipient count).
- Ask the user:
"Send this research note to your distribution list (N recipients)? [y/n]" — also show the first 3 recipients to confirm the right list.
- Parse the answer.
y / yes / send → call send_note(note_path). Anything else → skip.
- Return
{action: "sent" | "skipped", ...}.
auto_send(note_path, surface)
Non-interactive path for scheduled-agent-runner step 11.
- If
surface != "digest" → no-op, return {action: "noop", reason: "non-digest surface"}.
- Load + parse the distribution list. Missing/empty → stop-and-report (surfaces in runner summary as
email_failed=...).
- Call
send_note(note_path). Return its result with action: "sent".
This helper never prompts — non-interactive scheduled context. Misconfig still stop-and-reports per the rules below; the digest itself remains written.
show_list()
User-facing diagnostic. Loads, parses, and prints the distribution-list contents — what would happen on the next send. No mail traffic. Useful before relying on auto-send.
Parsing
The distribution-list file is Markdown. The parser scopes recipient extraction to a single named section so that documentation, format examples, and prose around the list never leak into the send:
- Strip YAML frontmatter (the
---\n...\n---\n block at the top, if present).
- Strip HTML comments (
<!-- ... -->) — anything inside is ignored, so commenting out a bullet pauses the recipient.
- Find the
## Recipients heading (H2, exact title, case-insensitive). Begin recipient-parsing only at the line after this heading.
- Stop recipient-parsing at the next
## heading (any title) or at end-of-file. Subsections inside ## Recipients using ### headings remain in-scope (groupings inside the list are fine).
- Inside the active region, a line contributes a recipient if:
- It starts with
- or * (a Markdown bullet) AND contains an email-shaped substring, OR
- The entire line, after stripping whitespace, IS an email-shaped substring (no leading bullet required).
- Lines starting with
``` (fenced code block boundary) are skipped, and the parser ignores everything between the open and close fence — code-block content is never a recipient.
- Email regex:
\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b.
- Multiple emails on one bullet line are all extracted (
- alice@example.com, bob@example.com works).
- Case-insensitive deduplication on the parsed list.
Sections OTHER than ## Recipients — including ## How to edit, ## Notes, the document intro, etc. — are scanned for emails for diagnostics only; addresses found there are reported by show_list as "ignored (outside ## Recipients)" but never sent to. This lets the template carry inline format examples (- you@gmail.com) without making them live entries.
Validation:
- File missing → stop-and-report (case #1 below).
## Recipients heading missing → stop-and-report: "email-distribution.md is missing the '## Recipients' heading. The skill only extracts addresses from bullets under that heading."
- Heading present but no parsed recipients → stop-and-report:
"'## Recipients' section is empty. Add at least one '- you@example.com' line under that heading."
Stop and report — enumerated cases
Each surfaces a structured error to the caller (and to the runner summary line for scheduled jobs):
- Distribution list missing →
"email-distribution.md not found at vault/_config/email-distribution.md. Copy .claude/skills/email-sender/email-distribution.example.md to that path and edit."
## Recipients heading missing → "email-distribution.md exists but has no '## Recipients' heading. The skill only extracts addresses from bullets under that exact heading (case-insensitive). Add a '## Recipients' section and list recipients as bullets under it."
## Recipients section is empty → "'## Recipients' section parsed to zero recipients. Add at least one '- you@example.com' bullet under that heading."
GMAIL_APP_PASSWORD missing → "GMAIL_APP_PASSWORD not set in ~/.config/research-bot/env. Run 'scripts/set-gmail-credentials.sh \"you@gmail.com\" \"xxxx xxxx xxxx xxxx\"' to set."
GMAIL_SEND_ADDRESS missing → similar.
- SMTP auth failure →
"Gmail SMTP auth failed for <send-address>. App password may be expired or revoked — regenerate at https://myaccount.google.com/apppasswords."
- SMTP send failure (network, server-level bounce) →
"Send failed: <smtplib error>. Per-recipient results: [sent=N, skipped=M]."
- Invalid email format on a recipient → skip that recipient, continue with the rest, surface in the return
skipped list (NOT a stop-and-report — degraded delivery proceeds).
The vault note remains written even if email fails. Email is a delivery channel, not a write-blocker.
Message body shape (v2 — HTML + Markdown attachment)
Built deterministically by render_and_send.py — no AI call in the send path, so scheduled launchd runs produce identical, testable output every time. The message is multipart/mixed:
multipart/mixed
├─ multipart/alternative
│ ├─ text/plain {note markdown, frontmatter stripped} + footer ← today's behavior
│ └─ text/html styled render of the same markdown + footer ← what mail clients show
└─ attachment {note}.md — the RAW vault file, frontmatter INCLUDED
HTML body: the note's Markdown (frontmatter stripped) rendered via Python-Markdown (extra, sane_lists, tables, fenced_code, toc) and wrapped in a self-contained template — an embedded <style> block, no external CSS/fonts/remote assets. Styling is restrained: system font stack, ~640px container, bordered/striped tables, monospace code blocks, muted footer. Renders cleanly in Gmail and Apple Mail (the self-send target).
Plain-text part: the raw Markdown + footer, unchanged from v1 — the fallback for clients that don't render HTML.
Attachment: the note file verbatim, including YAML frontmatter — a true copy of the canonical vault file, so the Markdown source is always preserved.
Footer (both parts):
---
Landed in your vault at: {vault_footer_path}
Sent via research-bot email-sender. Distribution list lives in your vault at _config/email-distribution.md.
Graceful fallback: if the markdown package isn't importable in the runtime python3, the script omits the HTML part and sends plain-text-only (v1 behavior) with the .md attachment still included, and returns "html": false. A missing dependency never blocks delivery — important for unattended scheduled runs. Install it via python3 -m pip install -r scripts/requirements.txt.
Subject derivation
| Source |
Subject template |
Example |
Digest (vault/digests/{cadence}/...) |
[{Cadence Title-Case} {Skill Title-Case}] {YYYY-MM-DD} — {first H1 of body, truncated to 60 chars} |
[Weekly Intelligence Digest] 2026-06-22 — Copilot Q3 roadmap & 3 new CVEs |
Research (vault/research/{topic}/...) |
[{Topic Title-Case} Research] {YYYY-MM-DD} — {frontmatter.title, truncated to 60 chars} |
[Copilot Research] 2026-06-22 — Agentic features & SR 11-7 implications |
| Override |
Use subject_override verbatim. |
n/a |
Truncation at 60 chars + … keeps the subject readable. Note frontmatter title is the source of truth when present; H1 of body is the fallback.
Composes with
vault-writer — successful return triggers email-sender invocation. vault-writer does NOT call email-sender; the calling skill makes the explicit call.
scheduled-agent-runner — step 11 calls auto_send.
- Category 1 researchers (
copilot-deep-dive, sdlc-best-practice, financial-regulator-watch, ai-governance-research, peer-bank-tech-intel, incident-postmortem-research, copilot-faq-answerer, …) — call prompt_then_send after vault-writer.write_research succeeds.
Acceptance test (single SMTP round-trip)
- Install the renderer dependency:
python3 -m pip install -r scripts/requirements.txt (gives the runtime python3 the markdown package).
- Create a minimal
email-distribution.md in the vault with one bullet pointing to the user's own email.
- Pick any existing digest in
~/Obsidian/Research-Brain/digests/ (ideally one with a table and a code block, to exercise the renderer).
- Invoke:
email-sender.send_note("<that path>").
- Confirm: email lands in inbox within ~10 seconds; subject follows the template; the body renders as styled HTML (headings, tables, code all formatted — not raw
#/| ASCII); footer references the vault path; and a .md attachment is present whose bytes equal the vault note (frontmatter included). Result JSON reports "html": true and "attached": "<note>.md".
- Fallback drill: run
render_and_send.py under a python3 without markdown installed (or temporarily force the ImportError path); confirm the mail still sends plain-text-only with the .md attached and the result reports "html": false — no crash.
- Misconfig drill: remove
GMAIL_APP_PASSWORD from env; re-invoke; confirm the missing_app_password stop-and-report (case #4) surfaces exactly as documented.
- Bad-recipient drill: add
bad@@invalid to the list; send; confirm the valid recipient gets mail and skipped lists the bad entry.
- Pause drill: wrap a bullet with
<!-- ... -->; re-parse; confirm that address is excluded.
1---2name: email-sender3description: Send, or preview and show, email delivery of a vault note (digest or research) to your distribution list. Delivery is deterministic — Markdown rendered to styled HTML with the raw `.md` attached, via a committed `render_and_send.py` (no AI call). Scheduled digests auto-send to everyone on the list; research notes prompt `[y/n]` first. A `show_list` action loads and prints the distribution list — who would receive the next send — without sending anything. Recipients come from a plain-Markdown list at `vault/_config/email-distribution.md`. Use when the user asks to email, send, forward, or distribute a digest or research note to their list, OR to show, list, view, preview, check, or validate their email distribution list (who's on it, who would get the next digest) — and immediately after `vault-writer.write_digest` or `vault-writer.write_research` succeeds. Not for composing an ad-hoc personal email or answering a generic SMTP question.4---56# email-sender78The post-write distribution hook. Every vault write worth surfacing beyond the local Obsidian copy goes through here. Scheduled digests fire-and-forget to the distribution list; ad-hoc research notes prompt the user `[y/n]` before sending. The vault note remains the canonical record; email is a delivery channel, not a replacement.910## When to use1112- Immediately after `vault-writer.write_digest()` succeeds — invoked from `scheduled-agent-runner` step 11.13- Immediately after `vault-writer.write_research()` succeeds in any Category 1 researcher — researcher invokes `prompt_then_send`.14- User explicitly asks: "email this", "send the latest weekly digest to my distribution list", "show me my distribution list".1516## When NOT to use1718- Writes to `facts/`, `events/`, `decisions/`, `insights/`, `people/`, `projects/`, `_inbox/` — durable knowledge, not distribution targets. (`auto_send` no-ops on these surfaces.)19- Non-interactive context AND surface is `research` — `prompt_then_send` requires a user; degrade gracefully by skipping.20- Mid-stream (vault write hasn't completed yet — wait for the write).2122## Prerequisites2324Two pieces of config:25261. **Gmail credentials** at `~/.config/research-bot/env`:27 ```28 GMAIL_SEND_ADDRESS=you@gmail.com29 GMAIL_APP_PASSWORD=xxxxxxxxxxxxxxxx30 ```31 Requires 2-Step Verification enabled on the account. Generate at https://myaccount.google.com/apppasswords ("Mail" → "Other / research-bot"). Use the helper script: `scripts/set-gmail-credentials.sh "you@gmail.com" "xxxx xxxx xxxx xxxx"` (note the leading space to keep the command out of shell history).32332. **Distribution list** at `vault/_config/email-distribution.md`. Copy [`email-distribution.example.md`](./email-distribution.example.md) to that path on first use and edit.3435If either is missing, the skill stops and reports — never silently drops.3637## Helpers3839### `send_note(note_path, subject_override=None)`4041The primary action. Steps:42431. Resolve `note_path` to an absolute path; confirm the file exists.442. Load and parse `vault/_config/email-distribution.md` (see [Parsing](#parsing)).453. Confirm `~/.config/research-bot/env` carries `GMAIL_SEND_ADDRESS` and `GMAIL_APP_PASSWORD` (the script re-reads them, but check here so a missing credential surfaces as a clean stop-and-report referencing the setup helper).464. Derive the **Subject**: `subject_override` if given, else derive from the note (see [Subject derivation](#subject-derivation)).475. Per-recipient validation: drop any address that doesn't match `^[^\s@]+@[^\s@]+\.[^\s@]+$` (record it for the `skipped` list). A single bad entry doesn't lose the rest.486. Invoke **`render_and_send.py`** (in this skill folder), piping a JSON payload on stdin:49 ```json50 {"note_path": "<abs path>", "subject": "<derived subject>",51 "bcc": ["<validated recipient>", "…"],52 "vault_footer_path": "digests/weekly/2026-…md"}53 ```54 The script builds the message and sends it: **From**/**To** = `GMAIL_SEND_ADDRESS` (self), **Bcc** = the validated recipients (addresses are NOT disclosed to each other — `send_message` strips the Bcc header before transmit). It renders the note's Markdown (frontmatter stripped) to styled HTML, attaches the raw `.md`, and emits a JSON result on stdout (or an `error`/`error_type` object + non-zero exit on failure — map `error_type` to the matching stop-and-report case below). See [Message body shape](#message-body-shape-v2--html--markdown-attachment).5556Return:5758```python59{60 "sent_to": ["addr1@example.com", "addr2@example.com"], # from the script's result61 "skipped": [{"email": "bad@@invalid", "reason": "invalid format"}], # from step 562 "subject": "[Weekly Intelligence Digest] 2026-06-22 — ...",63 "from": "you@gmail.com",64 "html": true, # false → markdown lib absent, plain-text-only fallback65 "attached": "2026-06-22-weekly-intelligence-digest.md"66}67```6869### `prompt_then_send(note_path)`7071For research notes. Asks the user, then calls `send_note` or skips.72731. Load + parse the distribution list (so the prompt can show the recipient count).742. Ask the user: `"Send this research note to your distribution list (N recipients)? [y/n]"` — also show the first 3 recipients to confirm the right list.753. Parse the answer. `y` / `yes` / `send` → call `send_note(note_path)`. Anything else → skip.764. Return `{action: "sent" | "skipped", ...}`.7778### `auto_send(note_path, surface)`7980Non-interactive path for `scheduled-agent-runner` step 11.81821. If `surface != "digest"` → no-op, return `{action: "noop", reason: "non-digest surface"}`.832. Load + parse the distribution list. Missing/empty → stop-and-report (surfaces in runner summary as `email_failed=...`).843. Call `send_note(note_path)`. Return its result with `action: "sent"`.8586This helper **never prompts** — non-interactive scheduled context. Misconfig still stop-and-reports per the rules below; the digest itself remains written.8788### `show_list()`8990User-facing diagnostic. Loads, parses, and prints the distribution-list contents — what would happen on the next send. No mail traffic. Useful before relying on auto-send.9192## Parsing9394The distribution-list file is Markdown. The parser scopes recipient extraction to a single named section so that documentation, format examples, and prose around the list never leak into the send:95961. Strip YAML frontmatter (the `---\n...\n---\n` block at the top, if present).972. Strip HTML comments (`<!-- ... -->`) — anything inside is ignored, so commenting out a bullet pauses the recipient.983. Find the `## Recipients` heading (H2, exact title, case-insensitive). Begin recipient-parsing **only** at the line after this heading.994. Stop recipient-parsing at the next `## ` heading (any title) or at end-of-file. Subsections inside `## Recipients` using `###` headings remain in-scope (groupings inside the list are fine).1005. Inside the active region, a line contributes a recipient if:101 - It starts with `-` or `*` (a Markdown bullet) AND contains an email-shaped substring, OR102 - The entire line, after stripping whitespace, IS an email-shaped substring (no leading bullet required).1036. Lines starting with ` ``` ` (fenced code block boundary) are skipped, and the parser ignores everything between the open and close fence — code-block content is never a recipient.1047. Email regex: `\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b`.1058. Multiple emails on one bullet line are all extracted (`- alice@example.com, bob@example.com` works).1069. Case-insensitive deduplication on the parsed list.107108Sections OTHER than `## Recipients` — including `## How to edit`, `## Notes`, the document intro, etc. — are scanned for emails for diagnostics only; addresses found there are reported by `show_list` as "ignored (outside ## Recipients)" but never sent to. This lets the template carry inline format examples (`- you@gmail.com`) without making them live entries.109110Validation:111112- File missing → stop-and-report (case #1 below).113- `## Recipients` heading missing → stop-and-report: `"email-distribution.md is missing the '## Recipients' heading. The skill only extracts addresses from bullets under that heading."`114- Heading present but no parsed recipients → stop-and-report: `"'## Recipients' section is empty. Add at least one '- you@example.com' line under that heading."`115116## Stop and report — enumerated cases117118Each surfaces a structured error to the caller (and to the runner summary line for scheduled jobs):1191201. **Distribution list missing** → `"email-distribution.md not found at vault/_config/email-distribution.md. Copy .claude/skills/email-sender/email-distribution.example.md to that path and edit."`1212. **`## Recipients` heading missing** → `"email-distribution.md exists but has no '## Recipients' heading. The skill only extracts addresses from bullets under that exact heading (case-insensitive). Add a '## Recipients' section and list recipients as bullets under it."`1223. **`## Recipients` section is empty** → `"'## Recipients' section parsed to zero recipients. Add at least one '- you@example.com' bullet under that heading."`1234. **`GMAIL_APP_PASSWORD` missing** → `"GMAIL_APP_PASSWORD not set in ~/.config/research-bot/env. Run 'scripts/set-gmail-credentials.sh \"you@gmail.com\" \"xxxx xxxx xxxx xxxx\"' to set."`1245. **`GMAIL_SEND_ADDRESS` missing** → similar.1256. **SMTP auth failure** → `"Gmail SMTP auth failed for <send-address>. App password may be expired or revoked — regenerate at https://myaccount.google.com/apppasswords."`1267. **SMTP send failure (network, server-level bounce)** → `"Send failed: <smtplib error>. Per-recipient results: [sent=N, skipped=M]."`1278. **Invalid email format on a recipient** → skip that recipient, continue with the rest, surface in the return `skipped` list (NOT a stop-and-report — degraded delivery proceeds).128129The vault note remains written even if email fails. Email is a delivery channel, not a write-blocker.130131## Message body shape (v2 — HTML + Markdown attachment)132133Built deterministically by `render_and_send.py` — no AI call in the send path, so scheduled `launchd` runs produce identical, testable output every time. The message is `multipart/mixed`:134135```136multipart/mixed137├─ multipart/alternative138│ ├─ text/plain {note markdown, frontmatter stripped} + footer ← today's behavior139│ └─ text/html styled render of the same markdown + footer ← what mail clients show140└─ attachment {note}.md — the RAW vault file, frontmatter INCLUDED141```142143- **HTML body**: the note's Markdown (frontmatter stripped) rendered via Python-Markdown (`extra`, `sane_lists`, `tables`, `fenced_code`, `toc`) and wrapped in a self-contained template — an embedded `<style>` block, no external CSS/fonts/remote assets. Styling is restrained: system font stack, ~640px container, bordered/striped tables, monospace code blocks, muted footer. Renders cleanly in Gmail and Apple Mail (the self-send target).144- **Plain-text part**: the raw Markdown + footer, unchanged from v1 — the fallback for clients that don't render HTML.145- **Attachment**: the note file **verbatim, including YAML frontmatter** — a true copy of the canonical vault file, so the Markdown source is always preserved.146- **Footer** (both parts):147148 ```149 ---150 Landed in your vault at: {vault_footer_path}151 Sent via research-bot email-sender. Distribution list lives in your vault at _config/email-distribution.md.152 ```153154**Graceful fallback**: if the `markdown` package isn't importable in the runtime `python3`, the script omits the HTML part and sends **plain-text-only** (v1 behavior) with the `.md` attachment still included, and returns `"html": false`. A missing dependency never blocks delivery — important for unattended scheduled runs. Install it via `python3 -m pip install -r scripts/requirements.txt`.155156## Subject derivation157158| Source | Subject template | Example |159|--------|------------------|---------|160| Digest (`vault/digests/{cadence}/...`) | `[{Cadence Title-Case} {Skill Title-Case}] {YYYY-MM-DD} — {first H1 of body, truncated to 60 chars}` | `[Weekly Intelligence Digest] 2026-06-22 — Copilot Q3 roadmap & 3 new CVEs` |161| Research (`vault/research/{topic}/...`) | `[{Topic Title-Case} Research] {YYYY-MM-DD} — {frontmatter.title, truncated to 60 chars}` | `[Copilot Research] 2026-06-22 — Agentic features & SR 11-7 implications` |162| Override | Use `subject_override` verbatim. | n/a |163164Truncation at 60 chars + `…` keeps the subject readable. Note frontmatter `title` is the source of truth when present; H1 of body is the fallback.165166## Composes with167168- [`vault-writer`](../vault-writer/SKILL.md) — successful return triggers email-sender invocation. vault-writer does NOT call email-sender; the calling skill makes the explicit call.169- [`scheduled-agent-runner`](../scheduled-agent-runner/SKILL.md) — step 11 calls `auto_send`.170- Category 1 researchers (`copilot-deep-dive`, `sdlc-best-practice`, `financial-regulator-watch`, `ai-governance-research`, `peer-bank-tech-intel`, `incident-postmortem-research`, `copilot-faq-answerer`, …) — call `prompt_then_send` after `vault-writer.write_research` succeeds.171172## Acceptance test (single SMTP round-trip)1731740. Install the renderer dependency: `python3 -m pip install -r scripts/requirements.txt` (gives the runtime `python3` the `markdown` package).1751. Create a minimal `email-distribution.md` in the vault with one bullet pointing to the user's own email.1762. Pick any existing digest in `~/Obsidian/Research-Brain/digests/` (ideally one with a table and a code block, to exercise the renderer).1773. Invoke: `email-sender.send_note("<that path>")`.1784. Confirm: email lands in inbox within ~10 seconds; subject follows the template; the body **renders as styled HTML** (headings, tables, code all formatted — not raw `#`/`|` ASCII); footer references the vault path; and a **`.md` attachment** is present whose bytes equal the vault note (frontmatter included). Result JSON reports `"html": true` and `"attached": "<note>.md"`.1795. Fallback drill: run `render_and_send.py` under a `python3` without `markdown` installed (or temporarily force the `ImportError` path); confirm the mail still sends **plain-text-only** with the `.md` attached and the result reports `"html": false` — no crash.1806. Misconfig drill: remove `GMAIL_APP_PASSWORD` from env; re-invoke; confirm the `missing_app_password` stop-and-report (case #4) surfaces exactly as documented.1817. Bad-recipient drill: add `bad@@invalid` to the list; send; confirm the valid recipient gets mail and `skipped` lists the bad entry.1828. Pause drill: wrap a bullet with `<!-- ... -->`; re-parse; confirm that address is excluded.