Skip to content

feat(mail): HTML lint lib + Larksuite-native autofix + lark-mail skill#787

Open
bubbmon233 wants to merge 17 commits into
larksuite:mainfrom
bubbmon233:feat/lark-mail
Open

feat(mail): HTML lint lib + Larksuite-native autofix + lark-mail skill#787
bubbmon233 wants to merge 17 commits into
larksuite:mainfrom
bubbmon233:feat/lark-mail

Conversation

@bubbmon233

@bubbmon233 bubbmon233 commented May 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds an HTML lint library + Larksuite-native autofix to lark-cli mail, plus the skills/lark-mail/ skill bundle (2 reference docs, 5 HTML templates, the +lint-html shortcut, and writing-path lint integration across all 6 compose shortcuts).

What's in this PR

1. Lint library (shortcuts/mail/lint/)

3-tier rule set:

  • Error: drop dangerous tags (<script> / <iframe> / <form> / <input> / <link> / <object> / <embed>), on* event handlers, and javascript: / vbscript: / file: URLs.
  • Warning + autofix: rewrite HTML4-era tags (<font> / <center> / <marquee> / <blink>).
  • Larksuite-native autofix: rewrite <p> / <ul> / <ol> / <li> / <blockquote> / <a> to mail-editor native markup so AI can write the simplest HTML and still produce native-quality rendering.
  • Inline-style and URL-scheme allow-list filtering.
  • <style> block passthrough (server adds CSS scope class).

2. +lint-html shortcut (preview / CI)

Read-only HTML preview tool. Default envelope returns only cleaned_html; --show-lint-details adds full warnings[] / errors[]. --strict exits non-zero on any finding (CI gate).

3. Writing-path lint in the 6 compose shortcuts

+send / +draft-create / +reply / +reply-all / +forward / +draft-edit body op all run lint before drafting:

  • lint_applied_count / original_blocked_count — always present.
  • lint_applied[] / original_blocked[] — only with --show-lint-details.
  • compose_hint — points AI consumers to the HTML writing guide.

4. skills/lark-mail/ skill bundle

  • 5 pre-rendered Larksuite-native HTML templates: weekly newsletter, personal weekly report, team weekly report, market research report, résumé.
  • 2 reference docs:
    • references/lark-mail-html.md — writing rules + format primitives + template-usage flow.
    • references/lark-mail-lint-html.md+lint-html usage + return-value contract + 9 examples.
  • SKILL.md updates linking the new docs and templates.

5. Sealed conventions

Fixed writing conventions enforced by the lint library, the Larksuite mail-editor data model, or the upstream service-side sanitiser.

  • @user mention chipid="at-user-N" is the only hard requirement; do not write data-user-id.
  • Highlight palette — 3 colors (pink milestones, yellow follow-ups, green completed); black text, no bold / padding / border-radius.
  • Brand color palette — main black, 3 levels of grey, Lark blue / deep blue, alert red, emergency orange, light pink / light grey backgrounds, border grey.
  • URL scheme allow-listhttp(s): / mailto: / cid: / data:image/* only.
  • Inline style allow-list — font-* / color / background-color / text-* / line-height / letter-spacing / vertical-align / margin* / padding* / width / height / display / border* / list-style* / white-space / word-break / overflow / transition / cursor / opacity.
  • Tag allow-list<p> / <div> / <span> / <a> / <img> / <table> (with <thead> / <tbody> / <tfoot> / <tr> / <td> / <th> / <caption> / <colgroup> / <col>) / <ul> / <ol> / <li> / <blockquote> / <h1>-<h6> / <b> / <i> / <em> / <strong> / <u> / <s> / <sub> / <sup> / <pre> / <code> / <style>.
  • Writing-style floor — subject ≤ 50 chars; decision-first; lists instead of "一、二、三" / "①②③"; emoji only as status tags; greeting / sign-off ≤ 1 paragraph each.

Tests

  • shortcuts/mail/lint/... — unit tests for every rule.
  • shortcuts/mail/mail_lint_html_test.go+lint-html envelope contract.
  • shortcuts/mail/mail_lint_writepath_test.go — writing-path envelope contract.
  • 5 templates verified via +draft-create smoke test.

Test plan

  • go test ./shortcuts/mail/lint/... ./shortcuts/mail/...
  • All 5 templates render correctly via +draft-create smoke
  • Default vs --show-lint-details envelope verified for both +lint-html and +draft-create

Summary by CodeRabbit

  • New Features

    • Added +lint-html shortcut for local HTML email linting (auto-fix by default; strict mode returns non-zero on findings).
    • Compose shortcuts now run automatic HTML sanitization during send/draft flows and accept --show-lint-details to expose findings.
    • --auto-fix / --strict options supported; cleaned HTML returned when auto-fix enabled.
  • Documentation

    • New comprehensive email HTML/CSS writing guidelines and +lint-html usage guide.
  • Templates

    • Added multiple HTML email templates (job application, newsletters, reports, weekly reports).

Review Change Stack

@github-actions github-actions Bot added domain/mail PR touches the mail domain size/XL Architecture-level or global-impact change labels May 8, 2026
@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces a complete HTML linting and auto-fixing pipeline for email composition across the Feishu mail CLI. It adds a new +lint-html command, integrates write-path HTML sanitization into all compose shortcuts, publishes comprehensive email writing guidelines, and contributes new email templates for common workflows.

Changes

Core Lint Engine and Integration

Layer / File(s) Summary
Type contracts and rule constants
shortcuts/mail/lint/types.go, shortcuts/mail/lint/rules.go
Defines Severity, Finding, Options, and Report types; exports rule ID constants for tags, attributes, styles, and Feishu-native rewrites.
Classification rules
shortcuts/mail/lint/rules.go
Tag allow/deny/warn classification, URL scheme validation (blocking javascript:, vbscript:, dangerous data:*, allowing data:image/*), inline style property allow-list with prefix matching (border-*, margin-*, padding-*), and event-handler attribute detection.
HTML linting engine and DOM operations
shortcuts/mail/lint/linter.go
Run() entry point with fragment parsing and empty-input handling; recursive DOM walk classifying tags into error-delete/warning-rewrite/pass-through tiers; attribute sanitization with URL and style validation; legacy tag rewriting (<font><span>, <center><div style="text-align:center">, <marquee>/<blink><span>); style property filtering with malformed declaration handling; excerpt generation with byte truncation.
Feishu-native style augmentation
shortcuts/mail/lint/linter.go
Post-processing pass ensuring Feishu renderer compatibility via list marker attributes, list-item numbering with data-ol-id/data-start, blockquote styles, link styles, paragraph wrapping into double-nested divs, text node canonicalization in Feishu span nesting, whitespace stripping, and attribute ordering.
Lint engine test suite
shortcuts/mail/lint/linter_test.go
45+ tests covering Tier-1 pass-through, Tier-2 warning rewrites and autofix behavior differences, Tier-3 dangerous tag removal, URL scheme blocking, style sanitization with malformed declaration handling, report contracts (non-nil empty arrays, CleanedHTML behavior), excerpt truncation, flag semantics, and regression tests (Feishu list margin preservation).
Write-path lint helpers
shortcuts/mail/mail_lint_writepath.go
--show-lint-details flag; runWritePathLint() helper that runs lint with AutoFix: true and Strict: false; applyLintToEnvelope() to conditionally populate envelope with finding arrays; empty lint helpers for non-lint branches; compose and draft-edit hint insertion utilities.
+lint-html shortcut
shortcuts/mail/mail_lint_html.go, shortcuts/mail/mail_lint_html_test.go
New read-only HTML linter shortcut with --body/--body-file input, --auto-fix (default true), --strict (error on warnings), --show-lint-details flags; envelope returns cleaned_html only when auto-fix is enabled and finding arrays only when details requested; includes path safety validation and --strict exit behavior; 13 tests validating flag constraints, envelope shapes, and exit codes.
Write-path integration and tests
shortcuts/mail/mail_lint_writepath_test.go
9 tests including runWritePathLint unit tests for empty/HTML input, applyLintToEnvelope contract tests (no fields in default mode, arrays in detail mode), and end-to-end +draft-create tests validating envelope shapes, safety (font rewriting, script removal), and EML payload integrity.
Compose shortcut integration
shortcuts/mail/mail_draft_create.go, mail_draft_edit.go, mail_forward.go, mail_reply.go, mail_reply_all.go, mail_send.go
Added showLintDetailsFlag to all compose shortcuts; refactored buildRawEMLForDraftCreate to return (rawEML, lintApplied, lintBlocked, err) with always-present non-nil lint slices; integrated runWritePathLint calls on HTML bodies after local image resolution and signature injection; applied lint envelopes and compose hints to stdout in both draft-saved and send paths.
Compose test updates
shortcuts/mail/mail_draft_create_test.go
11 test site updates to destructure four return values from buildRawEMLForDraftCreate while preserving existing EML assertions.
Shortcut registry
shortcuts/mail/shortcuts.go
Added MailLintHTML to the Shortcuts() function's returned list.

Documentation and Templates

Layer / File(s) Summary
HTML writing standards and mail guidelines
skills/lark-mail/references/lark-mail-html.md, skill-template/domains/mail.md
Comprehensive HTML/CSS/URL writing standards including style constraints (paragraph, title levels, text formatting, colors, line breaks, lists, tables, links, AT users, quotes, emphasis, alignment, box model, borders); URL schemes (external links, mailto, cid:, base64 data URIs); official template system (repository templates vs OAPI mail templates); AI workflow for template adaptation; lint return value contract; and mail domain safe-write constraints (no hand-pasted EML, mandatory lint pass via shortcuts).
Lint HTML command reference
skills/lark-mail/references/lark-mail-lint-html.md
Complete +lint-html documentation including local read-only behavior, parameter descriptions, envelope schema (default vs --show-lint-details modes, no count fields), finding structure, and error/warning example pairs showing input/output transformations.
Skill workflow and compose references
skills/lark-mail/SKILL.md, skills/lark-mail/references/lark-mail-*.md
SKILL.md: critical HTML reading prerequisites, optional +lint-html --strict pre-check, mail writing规范 section, template clarification, and +lint-html shortcut entry. Compose references (draft-create, draft-edit, forward, reply, reply-all, send): critical prerequisites requiring HTML writing guide reading before editing content.
Email template library
skills/lark-mail/assets/templates/*.html
Five new structured email templates: job-application--resume.html, newsletter--weekly-brief.html, research--market-report.html, weekly--personal-report.html, and weekly--team-report.html.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

enhancement

Suggested reviewers

  • infeng
  • chanthuang
  • haidaodashushu

Poem

🐰 I hopped through tags and trimmed the mess,
Rewrote old fonts to spans, removed the stress.
Lists now march tidy with Feishu-style grace,
Scripts are gone — your drafts wear a safer face.
Send on with care; your rabbit linted the place.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@CLAassistant

CLAassistant commented May 8, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@bubbmon233
bubbmon233 requested a review from chanthuang May 8, 2026 14:15
@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@e40070c342248dbac60e727be6dbdf9c9863bf10

🧩 Skill update

npx skills add bubbmon233/cli#feat/lark-mail -y -g

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (2)
shortcuts/mail/mail_lint_html_test.go (1)

157-183: ⚡ Quick win

LGTM — tests cleanly cover the --auto-fix=false / --show-lint-details envelope contract.

One gap to complement the issue raised in mail_lint_html.go: consider adding a sub-case to TestMailLintHTML_AutoFixFalseOmitsCleanedHTML that omits --show-lint-details and asserts on the resulting envelope shape (empty data, or explicitly documented fields), so the contract for that combination is pinned.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/mail/mail_lint_html_test.go` around lines 157 - 183, Add a sub-case
to TestMailLintHTML_AutoFixFalseOmitsCleanedHTML that calls
runMountedMailShortcut with the same flags except without "--show-lint-details"
(use MailLintHTML, runMountedMailShortcut and decodeShortcutEnvelopeData to
exercise the shortcut), then decode the envelope and assert the expected shape
for the --auto-fix=false + no --show-lint-details combination (e.g., that data
is empty or only contains the explicitly documented fields); keep assertions
deterministic and mirror existing test style so the contract is pinned alongside
the existing subcase.
shortcuts/mail/mail_lint_writepath_test.go (1)

15-17: 💤 Low value

Drop the double-helper indirection around json.Unmarshal.

jsonDecoderUnmarshal (line 17) and jsonUnmarshal (line 350) are both one-line wrappers that ultimately just call encoding/json.Unmarshal. The stated rationale ("to keep the import set explicit") is already achieved by a single wrapper or by calling json.Unmarshal directly at the one call site (line 340). The second indirection adds no value.

♻️ Proposed simplification
-// jsonDecoderUnmarshal is a thin alias used by helpers in this file to keep
-// the import set explicit even when the helper would otherwise be one-line.
-func jsonDecoderUnmarshal(b []byte, v interface{}) error { return json.Unmarshal(b, v) }
-
@@
 	var captured map[string]interface{}
-	if err := jsonUnmarshal(stub.CapturedBody, &captured); err != nil {
+	if err := json.Unmarshal(stub.CapturedBody, &captured); err != nil {
 		t.Fatalf("decode captured body: %v", err)
 	}
@@
-func jsonUnmarshal(b []byte, v interface{}) error {
-	return jsonDecoderUnmarshal(b, v)
-}

Also applies to: 350-352

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/mail/mail_lint_writepath_test.go` around lines 15 - 17, Remove the
unnecessary double-wrapper around encoding/json.Unmarshal: eliminate either
jsonDecoderUnmarshal or jsonUnmarshal and update callers to use the remaining
wrapper (or call json.Unmarshal directly). Specifically, remove the one-line
function jsonDecoderUnmarshal and replace its uses with jsonUnmarshal (or remove
jsonUnmarshal and replace its uses with json.Unmarshal) so there is only a
single indirection; update the call site referenced around line ~340 to call the
chosen function (jsonUnmarshal or json.Unmarshal) and remove the redundant
helper definition.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@shortcuts/mail/lint/linter.go`:
- Around line 500-505: truncateExcerpt currently slices by bytes
(s[:MaxExcerptBytes-4]) which can cut a UTF‑8 codepoint and produce invalid UTF
sequences; change it to truncate on rune boundaries instead by iterating runes
(for range over s) or converting to []rune and accumulating byte lengths until
reaching MaxExcerptBytes-4, then return the collected valid-prefix string + "
..."; update the function truncateExcerpt and keep MaxExcerptBytes logic but
ensure you never split a multi-byte rune.

In `@shortcuts/mail/mail_lint_html.go`:
- Around line 52-53: The flag description for "--auto-fix" is inaccurate: it
claims violations are returned when false but the code only emits
warnings/errors if "--show-lint-details" is set (see where warnings/errors
arrays are populated), so update the description in the flag definitions (the
entries that include Name: "auto-fix" and Name: "strict") to state that when
"--auto-fix=false" the cleaned_html is omitted and violation arrays are only
included if "--show-lint-details" is also set; also add a sub-case to
TestMailLintHTML_AutoFixFalseOmitsCleanedHTML that runs with "--auto-fix=false"
but without "--show-lint-details" and asserts the documented empty/omitted data
behavior.

In `@skill-template/domains/mail.md`:
- Around line 286-288: The two links in skill-template/domains/mail.md pointing
to references/lark-mail-html-allowlist.md and
references/lark-mail-feishu-native.md are broken because those files don't
exist; update the list to point to the existing references/lark-mail-html.md
(replace the allowlist link) and either add or remove the feishu-native
reference (replace with lark-mail-lint-html.md if that was intended or delete
the line), ensuring the link targets match the actual filenames
(references/lark-mail-html.md and references/lark-mail-lint-html.md) and that
link text reflects the replaced document.
- Around line 309-313: Update the mail skill doc to stop claiming `<style>` is
deleted: remove `<style>` from the "直接删除" list in skill-template/domains/mail.md
and add a one-line note that `<style>` blocks are passed through (see
shortcuts/mail/lint/types.go doc) and that CSS is scoped/handled server-side;
also mention classifyTag / blockedTags in shortcuts/mail/lint/rules.go for
accuracy so readers know `<style>` is allowed by the linter and templates like
skills/lark-mail/assets/templates/research--market-report.html rely on that
passthrough.

In `@skills/lark-mail/assets/templates/weekly--team-report.html`:
- Around line 8-11: The template contains invalid list nesting where child
<ul>/<ol> tags are direct children of other lists instead of being inside an
<li> (e.g., the repeated leading <ul data-list-bullet="true"> and the <ol ...
data-list-number="true"> that sit beside <li class="temp-li bullet2"> and <li
class="temp-li number1">); fix by moving each nested <ul> or <ol> so it is a
child of the appropriate preceding <li> (for example place the inner <ul
data-list-bullet="true"> that contains <li class="temp-li bullet2"> inside the
parent <li class="temp-li number1"> or the corresponding parent <li
class="temp-li bullet2">), remove any duplicated wrapper lists, and ensure every
<ul>/<ol> direct child is only <li> elements and any sublists are nested inside
those <li> elements (check class names like temp-li bullet2, temp-li bullet3,
and number1 to verify correct placement).

In `@skills/lark-mail/references/lark-mail-forward.md`:
- Around line 16-17: The line containing "编辑邮件内容前 MUST 先用 Read 工具读取
[references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**" has a
stray trailing bold marker and a broken relative link; remove the erroneous
closing "**" and correct the markdown link so the link text and target point to
the actual file name in the same directory (e.g., use lark-mail-html.md or
./lark-mail-html.md) ensuring the link is valid and the bold markers are
balanced.

In `@skills/lark-mail/references/lark-mail-html.md`:
- Around line 11-18: Replace the two typos in the guide text: change the phrase
"处于安全考虑" to "出于安全考虑" (the first sentence about the built-in HTML lint tool) and
change "不正文长度" to "不限制正文长度" (the "正文长度自适应" bullet) so the wording reads
correctly and preserves the original guidance.

In `@skills/lark-mail/references/lark-mail-reply-all.md`:
- Around line 16-17: The line containing "编辑邮件内容前 MUST 先用 Read 工具读取
[references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**" has a
stray trailing bold marker and a broken/duplicated relative link; remove the
extraneous "**" and ensure the Markdown link to references/lark-mail-html.md is
valid (single [text](references/lark-mail-html.md)) so the sentence reads
cleanly and the link resolves correctly in lark-mail-reply-all.md.

In `@skills/lark-mail/references/lark-mail-reply.md`:
- Around line 20-21: The markdown line starting with "编辑邮件内容前 MUST 先用 Read 工具读取
..." has malformed bold markup (missing the opening "**") and an incorrect
relative link; fix it by adding the opening "**" before the sentence and
changing the link target from "references/lark-mail-html.md" to
"lark-mail-html.md" so it points to the file in the same directory (update the
line content accordingly).

In `@skills/lark-mail/references/lark-mail-send.md`:
- Around line 15-16: The sentence fragment containing "编辑邮件内容前 MUST 先用 Read 工具读取
[references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**" has a
stray/malformed bold marker and a broken relative link; remove or correctly
place the trailing "**" and update the link target to the correct relative
filename (e.g. "lark-mail-html.md" or "./lark-mail-html.md") so the Markdown
link resolves, ensuring the text reads cleanly and the reference to
lark-mail-html.md is valid.

---

Nitpick comments:
In `@shortcuts/mail/mail_lint_html_test.go`:
- Around line 157-183: Add a sub-case to
TestMailLintHTML_AutoFixFalseOmitsCleanedHTML that calls runMountedMailShortcut
with the same flags except without "--show-lint-details" (use MailLintHTML,
runMountedMailShortcut and decodeShortcutEnvelopeData to exercise the shortcut),
then decode the envelope and assert the expected shape for the --auto-fix=false
+ no --show-lint-details combination (e.g., that data is empty or only contains
the explicitly documented fields); keep assertions deterministic and mirror
existing test style so the contract is pinned alongside the existing subcase.

In `@shortcuts/mail/mail_lint_writepath_test.go`:
- Around line 15-17: Remove the unnecessary double-wrapper around
encoding/json.Unmarshal: eliminate either jsonDecoderUnmarshal or jsonUnmarshal
and update callers to use the remaining wrapper (or call json.Unmarshal
directly). Specifically, remove the one-line function jsonDecoderUnmarshal and
replace its uses with jsonUnmarshal (or remove jsonUnmarshal and replace its
uses with json.Unmarshal) so there is only a single indirection; update the call
site referenced around line ~340 to call the chosen function (jsonUnmarshal or
json.Unmarshal) and remove the redundant helper definition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0b060b15-542c-4a8e-ab0d-1631f90a42f6

📥 Commits

Reviewing files that changed from the base of the PR and between 29a9896 and 094b999.

📒 Files selected for processing (31)
  • shortcuts/mail/lint/linter.go
  • shortcuts/mail/lint/linter_test.go
  • shortcuts/mail/lint/rules.go
  • shortcuts/mail/lint/types.go
  • shortcuts/mail/mail_draft_create.go
  • shortcuts/mail/mail_draft_create_test.go
  • shortcuts/mail/mail_draft_edit.go
  • shortcuts/mail/mail_forward.go
  • shortcuts/mail/mail_lint_html.go
  • shortcuts/mail/mail_lint_html_test.go
  • shortcuts/mail/mail_lint_writepath.go
  • shortcuts/mail/mail_lint_writepath_test.go
  • shortcuts/mail/mail_reply.go
  • shortcuts/mail/mail_reply_all.go
  • shortcuts/mail/mail_send.go
  • shortcuts/mail/shortcuts.go
  • skill-template/domains/mail.md
  • skills/lark-mail/SKILL.md
  • skills/lark-mail/assets/templates/job-application--resume.html
  • skills/lark-mail/assets/templates/newsletter--weekly-brief.html
  • skills/lark-mail/assets/templates/research--market-report.html
  • skills/lark-mail/assets/templates/weekly--personal-report.html
  • skills/lark-mail/assets/templates/weekly--team-report.html
  • skills/lark-mail/references/lark-mail-draft-create.md
  • skills/lark-mail/references/lark-mail-draft-edit.md
  • skills/lark-mail/references/lark-mail-forward.md
  • skills/lark-mail/references/lark-mail-html.md
  • skills/lark-mail/references/lark-mail-lint-html.md
  • skills/lark-mail/references/lark-mail-reply-all.md
  • skills/lark-mail/references/lark-mail-reply.md
  • skills/lark-mail/references/lark-mail-send.md

Comment on lines +500 to +505
func truncateExcerpt(s string) string {
if len(s) <= MaxExcerptBytes {
return s
}
return s[:MaxExcerptBytes-4] + " ..."
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

truncateExcerpt may split a UTF-8 codepoint at byte 196.

The byte slice at s[:MaxExcerptBytes-4] cuts at a fixed byte offset, which can land in the middle of a multi-byte UTF-8 sequence (common for Chinese subjects/bodies in this PR's templates). When the resulting string is JSON-encoded for the envelope, Go's encoder replaces the invalid trailing bytes with \ufffd, leaving a garbled-looking excerpt to AI/CI consumers.

Backing off to the previous rune boundary fixes it cheaply.

♻️ Suggested fix using rune-aware truncation
+import (
+    "unicode/utf8"
+)

 func truncateExcerpt(s string) string {
     if len(s) <= MaxExcerptBytes {
         return s
     }
-    return s[:MaxExcerptBytes-4] + " ..."
+    cut := MaxExcerptBytes - 4
+    // Back off to a rune boundary so we don't split a multi-byte sequence.
+    for cut > 0 && !utf8.RuneStart(s[cut]) {
+        cut--
+    }
+    return s[:cut] + " ..."
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/mail/lint/linter.go` around lines 500 - 505, truncateExcerpt
currently slices by bytes (s[:MaxExcerptBytes-4]) which can cut a UTF‑8
codepoint and produce invalid UTF sequences; change it to truncate on rune
boundaries instead by iterating runes (for range over s) or converting to []rune
and accumulating byte lengths until reaching MaxExcerptBytes-4, then return the
collected valid-prefix string + " ..."; update the function truncateExcerpt and
keep MaxExcerptBytes logic but ensure you never split a multi-byte rune.

Comment thread shortcuts/mail/mail_lint_html.go Outdated
Comment thread skill-template/domains/mail.md Outdated
Comment on lines +309 to +313
`+send` / `+draft-create` / `+reply` / `+reply-all` / `+forward` / `+draft-edit` body op 在调用 `emlbuilder` **之前**会强制对 HTML 正文做 lint:

- 错误(`<script>` / `on*` / `javascript:` URL / `<iframe>` / `<form>` / `<style>` / `<link>` 等)会被**直接删除**
- 警告(`<font>` / `<center>` / `<marquee>`)会被**自动修复**为飞书原生写法
- 不允许的 CSS property(`position` / `z-index` / `transform` 等)会从 inline `style` 里删除

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

<style> is documented as deleted but the lint implementation passes it through.

Line 311 lists <style> among the tags that are "直接删除" (directly deleted), but:

  • shortcuts/mail/lint/types.go package doc states: "<style> is passed through verbatim (Feishu mail server-side sanitiser handles it on render)".
  • shortcuts/mail/lint/rules.go does not include style in blockedTags, so classifyTag("style") falls through to the default "allow" branch.
  • The new skills/lark-mail/assets/templates/research--market-report.html template ships with a <style> block of class rules and depends on this passthrough behavior.

Since this skill doc is consumed by the AI agent to plan write paths, the false claim will lead it to avoid <style> blocks and miss the <style>-passthrough capability that templates rely on. Drop <style> from the deleted list (and ideally add a one-line note that <style> is passed through and CSS-scoped server-side).

📝 Suggested fix
-- 错误(`<script>` / `on*` / `javascript:` URL / `<iframe>` / `<form>` / `<style>` / `<link>` 等)会被**直接删除**
+- 错误(`<script>` / `on*` / `javascript:` URL / `<iframe>` / `<form>` / `<link>` 等)会被**直接删除**
+- `<style>` 块原样透传(服务端会自动加 CSS scope class,避免污染收件人邮箱样式)
 - 警告(`<font>` / `<center>` / `<marquee>`)会被**自动修复**为飞书原生写法
 - 不允许的 CSS property(`position` / `z-index` / `transform` 等)会从 inline `style` 里删除
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skill-template/domains/mail.md` around lines 309 - 313, Update the mail skill
doc to stop claiming `<style>` is deleted: remove `<style>` from the "直接删除" list
in skill-template/domains/mail.md and add a one-line note that `<style>` blocks
are passed through (see shortcuts/mail/lint/types.go doc) and that CSS is
scoped/handled server-side; also mention classifyTag / blockedTags in
shortcuts/mail/lint/rules.go for accuracy so readers know `<style>` is allowed
by the linter and templates like
skills/lark-mail/assets/templates/research--market-report.html rely on that
passthrough.

Comment thread skills/lark-mail/assets/templates/weekly--team-report.html Outdated
Comment on lines +16 to +17
编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Same two issues as in lark-mail-reply.md Line 20: malformed ** bold marker and broken relative link.

📝 Proposed fix
-编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**
+**编辑邮件内容前 MUST 先用 Read 工具读取 [lark-mail-html.md](lark-mail-html.md),其中包含邮件书写规范**
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**
**编辑邮件内容前 MUST 先用 Read 工具读取 [lark-mail-html.md](lark-mail-html.md),其中包含邮件书写规范**
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-mail/references/lark-mail-forward.md` around lines 16 - 17, The
line containing "编辑邮件内容前 MUST 先用 Read 工具读取
[references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**" has a
stray trailing bold marker and a broken relative link; remove the erroneous
closing "**" and correct the markdown link so the link text and target point to
the actual file name in the same directory (e.g., use lark-mail-html.md or
./lark-mail-html.md) ensuring the link is valid and the bold markers are
balanced.

Comment on lines +11 to +18
> 请注意,邮件内容编辑相关的 shortcut 内置 HTML lint 工具,处于安全考虑和格式适配,你输入的 HTML 可能会被自动调整

## 风格底线

- **邮件标题小于50字**: 邮件主题行 `--subject` 应控制在 50 字内,避免超长标题带来理解困难
- **多用列表、表格**:不要堆叠过长的文本段落,请擅长使用列表`<ul>` / `<ol>`或分段 `<p>`
- **列表书写规则**:**不要**用 `<p>一、...</p><p>二、...</p>` 这种「中文编号 + 段落」的列表样式,"①②③"、"1) 2) 3)的机械写法也请摒弃;请擅长使用列表格式 `<ul>` / `<ol>`。
- **正文长度自适应**:不正文长度,但要求**首屏要见到关键信息**。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix typos in the writing guide.

Two user-facing typos in this CRITICAL guide:

  • Line 11: 处于安全考虑 should be 出于安全考虑处于 means "to be in [a state]"; 出于 is the correct preposition for "out of/for [a reason]".
  • Line 18: 不正文长度 is missing a verb. From context (正文长度自适应:...,但要求首屏要见到关键信息), it should read 不限制正文长度.
📝 Proposed fix
-> 请注意,邮件内容编辑相关的 shortcut 内置 HTML lint 工具,处于安全考虑和格式适配,你输入的 HTML 可能会被自动调整
+> 请注意,邮件内容编辑相关的 shortcut 内置 HTML lint 工具,出于安全考虑和格式适配,你输入的 HTML 可能会被自动调整
@@
-- **正文长度自适应**:不正文长度,但要求**首屏要见到关键信息**。
+- **正文长度自适应**:不限制正文长度,但要求**首屏要见到关键信息**。
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
> 请注意,邮件内容编辑相关的 shortcut 内置 HTML lint 工具,处于安全考虑和格式适配,你输入的 HTML 可能会被自动调整
## 风格底线
- **邮件标题小于50字**: 邮件主题行 `--subject` 应控制在 50 字内,避免超长标题带来理解困难
- **多用列表、表格**:不要堆叠过长的文本段落,请擅长使用列表`<ul>` / `<ol>`或分段 `<p>`
- **列表书写规则****不要**`<p>一、...</p><p>二、...</p>` 这种「中文编号 + 段落」的列表样式,"①②③"、"1) 2) 3)的机械写法也请摒弃;请擅长使用列表格式 `<ul>` / `<ol>`
- **正文长度自适应**不正文长度,但要求**首屏要见到关键信息**
> 请注意,邮件内容编辑相关的 shortcut 内置 HTML lint 工具,出于安全考虑和格式适配,你输入的 HTML 可能会被自动调整
## 风格底线
- **邮件标题小于50字**: 邮件主题行 `--subject` 应控制在 50 字内,避免超长标题带来理解困难
- **多用列表、表格**:不要堆叠过长的文本段落,请擅长使用列表`<ul>` / `<ol>`或分段 `<p>`
- **列表书写规则****不要**`<p>一、...</p><p>二、...</p>` 这种「中文编号 + 段落」的列表样式,"①②③"、"1) 2) 3)的机械写法也请摒弃;请擅长使用列表格式 `<ul>` / `<ol>`
- **正文长度自适应**不限制正文长度,但要求**首屏要见到关键信息**
🧰 Tools
🪛 LanguageTool

[uncategorized] ~15-~15: 数词与名词之间一般应存在量词,可能缺少量词。
Context: ...,你输入的 HTML 可能会被自动调整 ## 风格底线 - 邮件标题小于50字: 邮件主题行 --subject 应控制在 50 字内,避免超长标题带...

(wa5)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-mail/references/lark-mail-html.md` around lines 11 - 18, Replace
the two typos in the guide text: change the phrase "处于安全考虑" to "出于安全考虑" (the
first sentence about the built-in HTML lint tool) and change "不正文长度" to
"不限制正文长度" (the "正文长度自适应" bullet) so the wording reads correctly and preserves
the original guidance.

Comment on lines +16 to +17
编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Same two issues as in lark-mail-reply.md Line 20: malformed ** bold marker and broken relative link.

📝 Proposed fix
-编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**
+**编辑邮件内容前 MUST 先用 Read 工具读取 [lark-mail-html.md](lark-mail-html.md),其中包含邮件书写规范**
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-mail/references/lark-mail-reply-all.md` around lines 16 - 17, The
line containing "编辑邮件内容前 MUST 先用 Read 工具读取
[references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**" has a
stray trailing bold marker and a broken/duplicated relative link; remove the
extraneous "**" and ensure the Markdown link to references/lark-mail-html.md is
valid (single [text](references/lark-mail-html.md)) so the sentence reads
cleanly and the link resolves correctly in lark-mail-reply-all.md.

Comment on lines +20 to +21
编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify where lark-mail-html.md actually lives
fd "lark-mail-html.md" skills/lark-mail/

# Confirm the path referenced in the link does NOT exist
ls skills/lark-mail/references/references/ 2>/dev/null && echo "WARNING: nested references/ dir exists" || echo "Nested path absent (expected)"

Repository: larksuite/cli

Length of output: 133


Fix malformed bold markup and broken relative link. The line has two issues: no opening ** for bold (only closing ** at end), and the link path references/lark-mail-html.md resolves to a non-existent nested directory. Since lark-mail-html.md exists in the same directory as this file, the link should be lark-mail-html.md without the references/ prefix.

Suggested fix
-编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**
+**编辑邮件内容前 MUST 先用 Read 工具读取 [lark-mail-html.md](lark-mail-html.md),其中包含邮件书写规范**
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**
**编辑邮件内容前 MUST 先用 Read 工具读取 [lark-mail-html.md](lark-mail-html.md),其中包含邮件书写规范**
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-mail/references/lark-mail-reply.md` around lines 20 - 21, The
markdown line starting with "编辑邮件内容前 MUST 先用 Read 工具读取 ..." has malformed bold
markup (missing the opening "**") and an incorrect relative link; fix it by
adding the opening "**" before the sentence and changing the link target from
"references/lark-mail-html.md" to "lark-mail-html.md" so it points to the file
in the same directory (update the line content accordingly).

Comment on lines +15 to +16
编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Same two issues as in lark-mail-reply.md Line 20: malformed ** bold marker and broken relative link.

📝 Proposed fix
-编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**
+**编辑邮件内容前 MUST 先用 Read 工具读取 [lark-mail-html.md](lark-mail-html.md),其中包含邮件书写规范**
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-mail/references/lark-mail-send.md` around lines 15 - 16, The
sentence fragment containing "编辑邮件内容前 MUST 先用 Read 工具读取
[references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**" has a
stray/malformed bold marker and a broken relative link; remove or correctly
place the trailing "**" and update the link target to the correct relative
filename (e.g. "lark-mail-html.md" or "./lark-mail-html.md") so the Markdown
link resolves, ensuring the text reads cleanly and the reference to
lark-mail-html.md is valid.

}
if id, ok := blockedTags[tag]; ok {
return "error", id
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: <svg> and <math> tags are not in blockedTags and fall through to return "allow", "". Both are well-known XSS vectors:

  • <svg onload="alert(1)"> — the onload handler gets stripped by processAttributes, but <svg><foreignObject><body onload="..."> may bypass this depending on how golang.org/x/net/html handles SVG namespace switching.
  • <math><mtext><table><mglyph><style><!--</style><img src onerror=...> — this is a classic mXSS (mutation XSS) pattern exploiting parser-context differences between HTML and MathML.
  • <svg><script>alert(1)</script></svg> — the inner <script> is caught by the walker, but the SVG element itself survives and could carry other active content.

Neither <svg> nor <math> are in the PR description's tag allow-list, so allowing them by default appears unintentional.

Severity: 🚫 Must Fix

Suggested Fix: Add <svg>, <math>, and their namespace-sensitive children (<foreignObject>, <mtext>, <mglyph>, <annotation-xml>) to blockedTags. If SVG inline images are needed, they should go through <img src="data:image/svg+xml;..."> (which is already allowed, though see the separate comment on SVG data URIs).

// don't trip the rule.
rest = strings.TrimSpace(rest)
if strings.HasPrefix(strings.ToLower(rest), "image/") {
return "ok", ""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: data:image/svg+xml URIs pass the allow-list because the check only verifies strings.HasPrefix(rest, "image/"). SVG data URIs can contain embedded <script> tags, event handlers, and <foreignObject> — they are a well-documented XSS vector (OWASP, PortSwigger). When rendered in an <img> tag, most modern browsers sandbox SVG and block script execution, but when used in CSS background-image or other contexts, the behavior is less predictable.

Severity: 🚫 Must Fix

Suggested Fix: Either block data:image/svg+xml specifically:

if strings.HasPrefix(strings.ToLower(rest), "image/") {
    if strings.HasPrefix(strings.ToLower(rest), "image/svg") {
        return "error", RuleAttrDataURLBlocked
    }
    return "ok", ""
}

Or, more conservatively, restrict data URIs to raster formats only (image/png, image/jpeg, image/gif, image/webp).

return -1
}
return r
}, value)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: The control-byte stripping only removes chars < 0x20 and 0x7F, but does not strip Unicode zero-width / invisible characters that can obfuscate URL schemes:

  • java\u200Bscript: (zero-width space U+200B inside "javascript") → extracted scheme is "java\u200Bscript" which does NOT match blockedURLSchemes["javascript"], so it falls to the default "warn" case instead of "error".
  • Similar bypasses via U+200C (ZWNJ), U+200D (ZWJ), U+FEFF (BOM), U+00A0 (NBSP), U+2028/U+2029 (line/paragraph separator), or bidi override characters (U+202A-U+202E).

Whether downstream renderers actually execute java\u200Bscript: depends on the mail client, but this is a defense-in-depth gap — the existing test TestRun_WhitespaceObfuscatedJavaScriptScheme only covers ASCII whitespace (\t, \n), not Unicode invisible chars.

Severity: 🚨 Should Fix

Suggested Fix: Extend the strings.Map filter to also strip Unicode categories Cf (format chars), Zs (space separators), and Zl/Zp (line/paragraph separators):

import "unicode"

value = strings.Map(func(r rune) rune {
    if r < 0x20 || r == 0x7F {
        return -1
    }
    if unicode.Is(unicode.Cf, r) || unicode.Is(unicode.Zs, r) ||
       unicode.Is(unicode.Zl, r) || unicode.Is(unicode.Zp, r) {
        return -1
    }
    return r
}, value)

return "ok", ""
}
return "error", RuleAttrJSURLBlocked
case blockedURLSchemes[scheme]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: When a data:text/html or file: URL is blocked, the rule ID is RuleAttrJSURLBlocked ("ATTR_JS_URL_BLOCKED"). This is semantically incorrect — a data:text/html URI is not a JavaScript URL, and file: is a local file access scheme. The finding's RuleID is part of the public envelope contract (per the S2 contract reference in the package comment), so consumers pattern-matching on rule IDs will misclassify these.

Severity: 🚨 Should Fix

Suggested Fix: Introduce distinct rule IDs:

RuleAttrDataURLBlocked = "ATTR_DATA_URL_BLOCKED"
RuleAttrFileURLBlocked = "ATTR_FILE_URL_BLOCKED"

And use them in the respective switch cases.

Comment thread shortcuts/mail/lint/linter.go Outdated
rep.Applied = append(rep.Applied, finding)
}
rewriteWarnTag(n, tagName)
// Recurse into the rewritten node by falling through; the

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: When opts.Strict = true AND opts.AutoFix = true, the finding is classified as SeverityError and added to rep.Blocked, but rewriteWarnTag is still called — the tag is rewritten (e.g., <font><span>), not removed. This is inconsistent with how true error-tier tags are handled (line 131: parent.RemoveChild(n)).

In strict mode, all findings are errors. A <font> tag classified as error should arguably be removed entirely (like <script>), not rewritten to <span>. The current behavior means strict mode reports it as an error but still produces a "fixed" output, which sends mixed signals to CI consumers using --strict.

Severity: 🚨 Should Fix

Suggested Fix: When opts.Strict is true, skip the rewrite and remove the tag's children (re-parent them) or remove the tag entirely, consistent with the error-tier treatment. Or, if the current behavior is intentional (strict = "report as error but still fix"), document this explicitly in the Options.Strict godoc and add a test asserting that strict-promoted warnings are rewritten, not removed.

· [YYYY-MM-DD]:调研完成日期(ISO 格式)
=============================================================================
-->
<style>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: This template relies entirely on a <style> block with CSS classes (display:flex, linear-gradient, box-shadow, gap, flex-wrap, etc.). Two problems:

  1. Doc inconsistency: skill-template/domains/mail.md line 288 states <style> blocks "会被直接删除" (will be deleted), but the linter actually passes <style> through (it's not in blockedTags). Either the doc or the code is wrong.
  2. Server-side risk: If the Feishu mail server-side sanitizer strips <style> blocks (as the doc claims), this template degrades to unstyled HTML — all CSS classes lose their definitions, display:flex layouts break, and the card/gradient design disappears. All other templates correctly use only inline styles.
  3. CSS properties outside allow-list: box-shadow, flex, gap, linear-gradient, flex-wrap are NOT in allowedStyleProps — they only work because they're in a <style> block (bypassing inline-style filtering), which makes this template fragile.

Severity: 🚨 Should Fix

Suggested Fix: Rewrite this template to use inline styles only (like the other 4 templates), or explicitly add <style> to allowedTags and update mail.md to document that <style> is allowed. If <style> is intentionally passed through (as the linter code suggests), the domain doc must be corrected.

// pass classifyURLValue. Lower-case canonical names.
var urlAttributes = map[string]bool{
"href": true,
"src": true,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 💡 Suggestion

Issue: srcset is missing from the URL attribute list. Since <img> passes through allowedTags, an <img srcset="javascript:..."> would not have its srcset value checked by classifyURLValue. While srcset has a more complex value syntax (comma-separated URL + descriptor pairs), the javascript: prefix would still be present in the raw value.

Also, the PR description's "Inline style allow-list" mentions letter-spacing but it's not in allowedStyleProps — minor doc/code discrepancy.

Suggested Fix: Add "srcset": true to urlAttributes. For letter-spacing, either add it to allowedStyleProps or remove it from the PR description's allow-list.

// End-to-end: +draft-create writing path emits envelope with lint fields.
// =====================================================================

// TestMailDraftCreate_WritePathLintEnvelopeDefault verifies +draft-create's

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 💡 Suggestion

Issue: The writepath E2E tests only cover +draft-create. The other 5 compose shortcuts (+send, +reply, +reply-all, +forward, +draft-edit) were all wired up with lint integration in this PR but lack dedicated writepath tests. While the shared helper functions (runWritePathLint, applyLintToEnvelope) are tested in isolation, the integration wiring in each shortcut's Execute function (flag registration, correct placement in the output pipeline, both draft/send branches) is not verified.

Suggested Fix: Add at least one E2E test per shortcut confirming the lint envelope fields appear in the output. For shortcuts with two branches (+forward/+reply/+reply-all/+send — draft-saved vs. sent), test both branches.

"word-wrap": true,
"overflow": true,
"overflow-wrap": true,
"vertical-align": true,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: From an email security perspective, the combination of allowed CSS properties enables several classic email abuse patterns that enterprise email gateways (Proofpoint, Mimecast, Barracuda) actively flag:

  1. Invisible phishing overlay: opacity:0 on an <a> tag creates a clickable but invisible link. Combined with cursor:pointer, the user clicks what they think is one element but actually hits a hidden link underneath.
  2. Hidden content: display:none hides content from visual rendering while it remains in the DOM — used for spam keyword stuffing, hidden tracking URLs, or payload hiding.
  3. Zero-size text: font-size:0 or font-size:1px makes text invisible — a classic spam technique to embed keywords or hidden URLs.
  4. Camouflaged text: color can be set identical to background-color (e.g., white-on-white), hiding text from readers but not from the DOM.
  5. Tracking pixel construction: width:0;height:0 or width:1px;height:1px on <img> with an external src creates a tracking pixel — the lint passes this through without warning.

These are not theoretical — they are documented in RFC 8058 (One-Click Unsubscribe), M3AAWG best practices, and OWASP email security guidelines.

Severity: 🚨 Should Fix

Suggested Fix: Add lint warnings (not errors — these have legitimate uses) for suspicious CSS patterns:

  • opacity:0 or opacity values very close to 0
  • display:none
  • font-size:0 or font-size:1px
  • <img> with width or height <= 2px (tracking pixel heuristic)

This doesn't require blocking — just surfacing them as SeverityWarning findings so consumers and users are aware.

for _, attr := range n.Attr {
name := strings.ToLower(attr.Key)

// 1. on*-handlers → always drop, error-tier.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: The attribute scanner does not check for link text vs. href mismatch — one of the most common phishing patterns in email. Example:

<a href="https://evil-site.com">https://your-company.com/dashboard</a>

The visible text looks like a legitimate internal URL, but the actual href points elsewhere. Enterprise email security gateways (Microsoft Defender for Office 365, Google Safe Browsing, Proofpoint URL Defense) specifically flag this pattern. Since this lint library aims to be a "safety floor" for the email writing path, detecting obvious URL-text mismatches would significantly strengthen its anti-phishing value.

Severity: 💡 Suggestion

Suggested Fix: When processing <a> tags, if the text content of the anchor looks like a URL (starts with http:// or https://), compare its domain with the href domain. If they differ, emit a warning finding like LINK_TEXT_HREF_DOMAIN_MISMATCH. This is a heuristic — it won't catch all phishing — but it catches the most common pattern with very low false-positive rate.

.gradient-header { background:linear-gradient(135deg, #1a73e8, #4285f4); border-radius:12px; padding:32px; color:white; text-align:center; }
.card { background-color:white; border-radius:8px; padding:20px; margin:16px 0; box-shadow:0 1px 3px rgba(0,0,0,0.1); }
.stat-row { display:flex; gap:10px; margin:16px 0; }
.stat-card { flex:1; background-color:white; border-radius:8px; padding:14px; text-align:center; box-shadow:0 1px 3px rgba(0,0,0,0.1); }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: display:flex and gap have zero support in Outlook (all versions — Outlook uses the Word HTML rendering engine, not a browser engine). This template's card-grid layout will collapse into a vertical stack with no gap spacing when opened in Outlook, which is the dominant email client in enterprise environments.

Specific incompatibilities in this template:

CSS Property Outlook Gmail (web) Apple Mail Yahoo
display:flex ⚠️ partial
gap
linear-gradient ⚠️
box-shadow
border-radius ❌ (Windows)
max-width on div

The email industry standard for multi-column layouts is <table> with inline styles — it's ugly to write but renders consistently across all clients. The other 4 templates in this PR correctly avoid these properties.

Severity: 🚨 Should Fix

Suggested Fix: Rewrite the card-grid sections (.stat-row, .player-row) using <table> with <td> for each card. Replace linear-gradient with a solid color fallback. Move all class-based styles to inline style attributes. Use Can I Email as the compatibility reference.

"section": true,
"article": true,
"header": true,
"footer": true,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Semantic HTML5 tags (<section>, <article>, <header>, <footer>, <nav>, <main>, <figure>, <figcaption>) are in allowedTags, but email clients handle them very inconsistently:

  • Outlook (Word engine): renders them as inline spans with no block behavior — layout completely breaks.
  • Gmail: strips <article>, <nav>, <main> entirely; keeps <section>/<header>/<footer> but removes their semantic meaning.
  • Yahoo Mail: strips most semantic tags.
  • AOL: strips all HTML5 semantic tags.

Email HTML is effectively a subset of HTML4 + CSS2.1. The industry best practice (per Litmus, Email on Acid, MJML framework) is to use only <div>, <span>, <table>, <p>, <h1>-<h6>, and formatting tags. Including semantic tags in the allow-list creates a false sense of safety — authors will use them thinking they're "allowed," but the rendered result in major email clients will be broken or stripped.

Severity: 💡 Suggestion

Suggested Fix: Either move <section>, <article>, <header>, <footer>, <nav>, <main>, <figure>, <figcaption> to warnAutofixTags (rewrite to <div>) or add a lint hint when these tags are used, noting that they have poor email client support. The reference doc lark-mail-html.md already doesn't mention them in any examples — implicitly acknowledging they shouldn't be used.

}
child := parent.FirstChild
for child != nil {
next := child.NextSibling

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: The applyFeishuNativeStyles pass adds native inline styles to <img> tags — but the lint does not enforce alt attribute presence on <img>. Missing alt text is both an accessibility failure (WCAG 2.1 Level A, criterion 1.1.1) and a practical email UX problem — most email clients block external images by default and display alt text as a fallback. An <img> without alt shows as a blank rectangle or broken icon until the recipient manually loads images.

This is not a niche concern: Google Workspace, Outlook, and Apple Mail all block images by default for external senders. Enterprise security policies often enforce image blocking. Alt text is the only way recipients see anything until they click "load images."

Severity: 💡 Suggestion

Suggested Fix: In processAttributes or as a post-pass, emit a SeverityWarning finding when an <img> tag lacks an alt attribute (or has alt=""). Rule ID suggestion: IMG_MISSING_ALT_TEXT.


> 请注意,邮件内容编辑相关的 shortcut 内置 HTML lint 工具,处于安全考虑和格式适配,你输入的 HTML 可能会被自动调整

## 风格底线

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Two inconsistencies between this doc and skill-template/domains/mail.md:

  1. Subject line length: This doc says "邮件标题小于50字" (line 13), but mail.md says "标题 ≤ 30 字" (line 302). These are contradictory. Industry standard is typically 30-50 chars depending on mobile vs. desktop — but the two docs in the same PR must agree.

  2. opacity example (line 209 of this doc): The writing guide includes <span style="opacity:0.5">半透明文字</span> as a recommended pattern. While semi-transparent text is fine, showing opacity as a first-class recommended feature invites misuse (opacity:0 for invisible content). Consider whether this example belongs in an email writing guide, given the phishing risk described in the separate security comment.

Severity: 🚨 Should Fix (for the inconsistency), 💡 Suggestion (for the opacity example)

Suggested Fix: Align both documents on one subject-length limit. If 50 is the intent, update mail.md; if 30, update this doc. For the opacity example, consider adding a note that opacity:0 will trigger a lint warning (if the invisible-content detection suggested in the security comment is implemented).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@shortcuts/mail/mail_lint_writepath_test.go`:
- Around line 20-23: Update the outdated header comment block that begins
"Writing-path lint integration tests — compose 5 + +draft-edit emit" to reflect
the current contract: state that write-path envelopes do NOT include lint arrays
by default and that `lint_applied[]` and `original_blocked[]` are emitted only
when detail mode is enabled; replace the sentence claiming they "always" appear
with a clear note about the default absence and the detail-mode exception so the
header matches the tests in this file.

In `@shortcuts/mail/mail_lint_writepath.go`:
- Around line 32-41: Update the stale header comment describing the writing-path
safety contract to reflect that lint arrays `lint_applied` and
`original_blocked` are emitted only when `showDetails` is enabled (not always
present); keep the rest of the contract (AutoFix always true, Strict always
false) intact. Edit the comment block near the top of mail_lint_writepath.go and
the secondary comment around the later block (previously lines 89-92) to
explicitly state that consumers must check `showDetails` before relying on
`data.lint_applied` or `data.original_blocked`, and reference the `showDetails`
flag and the envelope keys `lint_applied`/`original_blocked` so readers can find
the relevant logic paths.

In `@skills/lark-mail/references/lark-mail-lint-html.md`:
- Around line 41-42: The docs are inconsistent about when the process exits
non-zero: update the descriptions for `--strict` and `--show-lint-details` and
the envelope behavior so they match: state that `--strict` causes the linter to
treat any `warnings[]` or `errors[]` as fatal and exit non-zero, and clarify
that when `--show-lint-details` is true the envelope returns full `warnings[]`
and `errors[]` arrays (otherwise only `cleaned_html` is returned); ensure both
the flag docs and the envelope behavior mention the same exit condition
(non-empty `warnings[]` or `errors[]`) so CI callers can rely on it.
- Line 97: 标题 “Error 类(强制删除,写信链路也会拒)”
过分表述写信流程会被直接拒绝;请把该标题与正文改为描述实际行为:错误类操作会导致内容被强制删除或在未能通过 sanitize/auto-fix
后才会被拒绝写入(compose 阶段会先尝试 sanitize/auto-fix),例如将标题改为“Error 类(强制删除;写信前会尝试
sanitize/auto-fix,失败才拒)” 并在相关段落中补充一到两句说明 sanitize/auto-fix
在写信链路之前运行、仅在自动修复/校验失败时才会拒绝写入,以避免暗示默认硬拒绝。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e9340bdb-4aa8-41b3-84c9-495d32e33c4e

📥 Commits

Reviewing files that changed from the base of the PR and between fe001f2 and 97524a9.

📒 Files selected for processing (5)
  • shortcuts/mail/mail_draft_create.go
  • shortcuts/mail/mail_lint_writepath.go
  • shortcuts/mail/mail_lint_writepath_test.go
  • skills/lark-mail/references/lark-mail-html.md
  • skills/lark-mail/references/lark-mail-lint-html.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • shortcuts/mail/mail_draft_create.go

Comment on lines +20 to +23
// Writing-path lint integration tests — compose 5 + +draft-edit emit
// `lint_applied[]` and `original_blocked[]` arrays in the stdout envelope
// always (spec §4.3 contract).
// =====================================================================

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix outdated contract wording in the test header comment.

This header says write-path envelopes always emit lint arrays, but the tests in this file validate the opposite default behavior (no lint fields unless detail mode). Please update the comment to the current contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/mail/mail_lint_writepath_test.go` around lines 20 - 23, Update the
outdated header comment block that begins "Writing-path lint integration tests —
compose 5 + +draft-edit emit" to reflect the current contract: state that
write-path envelopes do NOT include lint arrays by default and that
`lint_applied[]` and `original_blocked[]` are emitted only when detail mode is
enabled; replace the sentence claiming they "always" appear with a clear note
about the default absence and the detail-mode exception so the header matches
the tests in this file.

Comment thread shortcuts/mail/mail_lint_writepath.go Outdated
Comment on lines +32 to +41
// The writing-path safety contract (spec §4.3) is:
// - AutoFix is ALWAYS true (no `--no-lint` opt-out); errors are dropped
// and warnings are auto-fixed in place.
// - Strict is ALWAYS false; warnings never bump the exit code on the
// write path (compare with `+lint-html --strict` which is a CI tool).
// - The returned report is appended to the writing-path stdout envelope
// under the contract keys `lint_applied` (warnings) and
// `original_blocked` (errors); both arrays are always present (possibly
// empty) so consumers can rely on `data.lint_applied[]` and
// `data.original_blocked[]` unconditionally.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update stale envelope-contract comments to match current behavior.

These comments still state lint arrays are always present, but the implementation now emits lint_applied / original_blocked only when showDetails is enabled. Please align the comments with the current contract to prevent downstream misuse.

Also applies to: 89-92

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/mail/mail_lint_writepath.go` around lines 32 - 41, Update the stale
header comment describing the writing-path safety contract to reflect that lint
arrays `lint_applied` and `original_blocked` are emitted only when `showDetails`
is enabled (not always present); keep the rest of the contract (AutoFix always
true, Strict always false) intact. Edit the comment block near the top of
mail_lint_writepath.go and the secondary comment around the later block
(previously lines 89-92) to explicitly state that consumers must check
`showDetails` before relying on `data.lint_applied` or `data.original_blocked`,
and reference the `showDetails` flag and the envelope keys
`lint_applied`/`original_blocked` so readers can find the relevant logic paths.

Comment on lines +41 to +42
| `--strict` | 否 | 默认 `false`。`true` 时把 warning 视作 error 并退出非零(CI 用) |
| `--show-lint-details` | 否 | 默认 `false`。`true` 时 envelope 同时返回 `warnings[]` / `errors[]` 完整 Finding 数组;默认仅返回 `cleaned_html`,避免复杂模板触发数十条装饰性 warning 把响应撑大几千 token |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Strict mode exit condition is described inconsistently.

Line 41 says strict treats warnings as errors and exits non-zero, but Line 81 implies only non-empty errors[] triggers non-zero. Please align these so callers can reliably implement CI checks.

Also applies to: 81-81

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-mail/references/lark-mail-lint-html.md` around lines 41 - 42, The
docs are inconsistent about when the process exits non-zero: update the
descriptions for `--strict` and `--show-lint-details` and the envelope behavior
so they match: state that `--strict` causes the linter to treat any `warnings[]`
or `errors[]` as fatal and exit non-zero, and clarify that when
`--show-lint-details` is true the envelope returns full `warnings[]` and
`errors[]` arrays (otherwise only `cleaned_html` is returned); ensure both the
flag docs and the envelope behavior mention the same exit condition (non-empty
`warnings[]` or `errors[]`) so CI callers can rely on it.


下面是用 `lark-cli mail +lint-html --body '<INPUT>' --show-lint-details` 实跑得到的典型 case(加 `--show-lint-details` 才能看到 finding;默认只返回 `cleaned_html`),覆盖 error 类(强制删)和 warning 类(自动修复)。

### Error 类(强制删除,写信链路也会拒)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

“写信链路也会拒” overstates write-path behavior.

This phrasing suggests compose shortcuts reject content, while the documented pipeline is sanitize/auto-fix before compose. Consider changing this to avoid implying default hard-fail behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-mail/references/lark-mail-lint-html.md` at line 97, 标题 “Error
类(强制删除,写信链路也会拒)” 过分表述写信流程会被直接拒绝;请把该标题与正文改为描述实际行为:错误类操作会导致内容被强制删除或在未能通过
sanitize/auto-fix 后才会被拒绝写入(compose 阶段会先尝试 sanitize/auto-fix),例如将标题改为“Error
类(强制删除;写信前会尝试 sanitize/auto-fix,失败才拒)” 并在相关段落中补充一到两句说明 sanitize/auto-fix
在写信链路之前运行、仅在自动修复/校验失败时才会拒绝写入,以避免暗示默认硬拒绝。

bubbmon233 and others added 12 commits May 20, 2026 16:31
Add an HTML lint library + Larksuite-native autofix to lark-cli mail, plus
the skills/lark-mail/ skill bundle (2 reference docs, 5 HTML templates, the
+lint-html shortcut, and writing-path lint integration across all 6 compose
shortcuts).

Lint library (shortcuts/mail/lint/)

- Error: drop dangerous tags (<script> / <iframe> / <form> / <input> /
  <link> / <object> / <embed>), on* event handlers, javascript: /
  vbscript: / file: URLs.
- Warning + autofix: rewrite HTML4-era <font> / <center> / <marquee> /
  <blink>.
- Larksuite-native autofix: rewrite <p> / <ul> / <ol> / <li> /
  <blockquote> / <a> to mail-editor native markup so AI can write the
  simplest HTML and still produce native-quality rendering.
- Inline-style and URL-scheme allow-list filtering.
- <style> block passthrough (server adds CSS scope class).

+lint-html shortcut (preview / CI)

Read-only HTML preview tool. Default envelope returns only cleaned_html;
--show-lint-details adds full warnings[] / errors[]. --strict exits non-
zero on any finding (CI gate).

Writing-path lint in 6 compose shortcuts

+send / +draft-create / +reply / +reply-all / +forward / +draft-edit body
op all run lint before drafting:

- lint_applied_count / original_blocked_count: always present.
- lint_applied[] / original_blocked[]: only with --show-lint-details.
- compose_hint: points AI consumers to the HTML writing guide.

skills/lark-mail/ skill bundle

5 pre-rendered Larksuite-native HTML templates: weekly newsletter,
personal weekly report, team weekly report, market research report,
résumé.

2 reference docs:
- references/lark-mail-html.md: writing rules + format primitives +
  template-usage flow.
- references/lark-mail-lint-html.md: +lint-html usage + return-value
  contract + 9 worked examples.

SKILL.md updates linking the new docs and templates.

Sealed conventions

- @user mention chip: id="at-user-N" is the only hard requirement; do
  not write data-user-id.
- Highlight palette: 3 colors (pink milestones, yellow follow-ups, green
  completed); black text, no bold / padding / border-radius.
- Brand color palette: main black, 3 levels of grey, Lark blue / deep
  blue, alert red, emergency orange, light pink / light grey
  backgrounds, border grey.
- URL scheme allow-list: http(s): / mailto: / cid: / data:image/* only.
- Inline-style + tag allow-lists.
- Writing-style floor: subject <= 50 chars, decision-first, lists instead
  of mechanical numbering, emoji only as status tags.

Tests

- shortcuts/mail/lint/...: unit tests for every rule.
- shortcuts/mail/mail_lint_html_test.go: +lint-html envelope contract.
- shortcuts/mail/mail_lint_writepath_test.go: writing-path envelope
  contract.
- 5 templates verified via +draft-create smoke test.
Move data["lint_applied_count"] / data["original_blocked_count"]
assignments inside the showDetails branch in applyLintToEnvelope so
all four lint fields enter and leave the envelope together. This
restores the default 3-key envelope (compose_hint / draft_id / reference)
for the compose-family shortcuts (+send / +reply / +reply-all /
+forward / +draft-create / +draft-edit) and keeps the four lint fields
behind --show-lint-details as the tech design intends.

sprint: S2
Remove tip field from buildDraftSavedOutput's returned map and flip the
test assertions in TestBuildDraftSavedOutputIncludesReferenceOnlyWhenPresent
to require tip absence. The compose-family default envelope (+send /
+reply / +reply-all / +forward) now stays within the 3-key contract
(compose_hint / draft_id / reference) defined in tech-design v1 §4.1.5.

hintSendDraft already writes the equivalent guidance to stderr, so no
UX regression — the message reaches the user via the dedicated stderr
hint channel instead of the structured stdout envelope.

sprint: S3
+draft-create now always attaches a fixed draft_edit_hint to its stdout
envelope (alongside compose_hint + draft_id), guiding callers to edit
the existing draft via +draft-edit --draft-id <id> instead of re-running
+draft-create and producing duplicate drafts. The hint is single-target:
only MailDraftCreate emits it; the other 5 compose shortcuts (+send,
+reply, +reply-all, +forward, +draft-edit) keep their existing envelope
shape unchanged.

applyLintToEnvelope no longer writes lint_applied_count or
original_blocked_count. Under --show-lint-details the envelope returns
only the two Finding arrays (lint_applied[] / original_blocked[]) —
callers needing a count compute it via len(arr). The change propagates
to all 6 compose shortcuts via the shared helper.

Tests, the shortcut flag description, and the lark-mail-html /
lark-mail-lint-html reference docs are updated to match.

sprint: S2
…tcut structs (PR 787 followup)

The 4 compose shortcuts (+send, +reply, +reply-all, +forward) were missing
the `HasFormat: true` field in their Shortcut struct literals, so cobra
parse rejected `--format json` with `unknown flag: --format`. This blocked
the JSON envelope output path (compose_hint / lint envelope) that the
verify suite exercises.

The fix mirrors the existing positive siblings in the same package
(mail_draft_create.go:43, mail_draft_edit.go:29, mail_lint_html.go:43):
add a single line `HasFormat:   true,` between `AuthTypes:` and `Flags:`
in each of the 4 Shortcut struct literals. No new manual --format flag
entry is added; the framework auto-registers --format via runner.go when
HasFormat == true.

Refs: verification_report §3.1 (fail_code_bug for INT-CLI-Send-DefaultHidesStrip-01)
+draft-edit only accepted body edits via --patch-file (set_body op),
causing "unknown flag: --body" when called the same way as +draft-create.
This adds --body as a convenience flag that translates directly into a
set_body patch op, making all mail compose shortcuts consistent.

- Add --body flag to MailDraftEdit.Flags
- In buildDraftEditPatch: if --body is set, prepend a set_body op;
  mutual-exclusion check prevents combining with --patch-file body ops
- Update patch template notes to reflect the new --body shorthand
- Existing lint pipeline (Execute loop over ops) already handles the
  new op: HTML is sanitized, envelope hides lint_applied/original_blocked
  by default unless --show-lint-details is passed

Fixes: INT-CLI-DraftEdit-DefaultHidesStrip-01 and dependent cases
Bug 1: Template HTML <li> elements with class "temp-li bullet2" or
"temp-li bullet3" were missing the "bullet1" class required by the
STYLE_LIST_ITEM_NATIVE_INLINE_APPLIED lint rule, causing 18+ warnings
in --strict mode for PersonalWeekly/TeamWeekly/Resume templates.
research--market-report.html used native <li> elements which were
fully converted to Feishu-native list format (ul/ol + li with all
required class, data-* attrs, style props and text span wrapping).

Bug 2: +send lacked --body-file flag, breaking the E2E workflow:
  +lint-html → save cleaned.html → +send --body-file ./cleaned.html
Added --body-file flag (mutually exclusive with --body) that reads
the HTML body from a file. Matches the pattern in +lint-html.
- Add class="not-doclink" to all 20 mention-chip <a id="at-user-N"> elements
  (fixes 20× STYLE_LINK_NATIVE_INLINE_APPLIED warnings in --strict mode)
- Change class="temp-li number2" → "temp-li number1 number2" on the two
  <li data-start="a|b"> sub-items in 下周工作 nested ol
  (fixes 2× STYLE_LIST_ITEM_NATIVE_INLINE_APPLIED warnings)
- Add start="1" to the outer wrapper <ol> of the nested weekly-next sub-list
  (fixes 1× STYLE_LIST_NATIVE_INLINE_APPLIED warning; ensureAttr saw start absent)

All 23 warnings eliminated; +lint-html --strict should now exit 0.
Replace 11 <p> tags with <div> elements (preserving inline styles) to
eliminate STYLE_PARA_WRAPPER_REWRITTEN warnings. The lint engine rewrites
<p> to Lark-native double-wrapped div paragraphs and emits a warning for
each; using <div> directly avoids the rewrite.

Also add class="not-doclink" + native link inline styles to 3 bare <a>
tags in the PR-reference section, eliminating STYLE_LINK_NATIVE_INLINE_APPLIED
warnings.

Verified with lint.Run() directly: blocked=0 applied=0 (0 findings).
All 5 templates now pass +lint-html --strict with 0 findings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Block + Major + selected Nit items from the 2026-05-20 code review.

lint lib (shortcuts/mail/lint):
- Drop opts.Strict / opts.AutoFix from Options; lib always autofixes
  warnings and removes errors (writing-path safety contract). Strip the
  corresponding branches from Run / processElement / processAttributes /
  applyFeishuNativeStyles.
- Make data-ol-id deterministic: per-Run nativeCtx maps each <ol> node to
  a positional index and nodeShortID hashes that index instead of the
  heap pointer. cleaned_html is now byte-stable across runs.
- processAttributes style branch: preserve attr.Val verbatim when no
  property dropped (avoid spurious whitespace differences on idempotent
  input).
- Add LIST_DIRECT_CHILD_NON_LI rule + autofix: wrap non-<li> direct
  children of <ul>/<ol> (notably <ul><ul> nesting) in a synthetic <li>.
- Scrub user-facing "RemoteSanitizer" / "server-side sanitizer" /
  "Lark mail-editor" wording from hints + code comments. Hint text now
  describes the client-side action only.

+lint-html (shortcuts/mail/mail_lint_html.go):
- Remove --strict and --auto-fix flags. cleaned_html always emitted;
  command never bumps exit code (preview / advisory tool).
- Drop the corresponding strict / autofix=false tests.

Writing-path (compose 5 + draft-edit):
- Extract shared readBodyFile() + validateBodyFileMutex() helpers with
  a 32 MB size cap (io.LimitReader). Both --body-file callers route
  through them.
- Add --body-file flag to +draft-create / +draft-edit / +reply /
  +reply-all / +forward (was only on +send). Each implements mutual
  exclusion with --body + cwd-subtree path safety.
- Fix +send body-file + --inline silent inline-image drop: Validate
  now resolves the body content first so the plain-text + inline
  combination errors out the same way --body 'plain' + --inline does.
- Remove unused lintFinding type alias; callers reference lint.Finding
  directly.

Templates (skills/lark-mail/assets/templates):
- Wrap inner <ul>/<ol> in <li> in weekly--team-report.html (4 places)
  and job-application--resume.html (4 places) so they conform to the
  new ul/ol direct-child rule and have valid HTML structure regardless.

Tests:
- Update TestRunWritePathLint_HTMLAlwaysAutofixedWarningNeverElevated
  contract (was the +strict variant) to assert never-elevated behaviour.
- Add e2e write-path lint tests for the 5 other compose shortcuts:
  +send / +reply / +reply-all / +forward / +draft-edit — each asserts
  the <font> autofix reaches the captured EML before the API call.
- Add plain-text + --show-lint-details corner case test that locks
  empty-but-non-nil arrays in the envelope.
- Add ul/ol direct-child-non-li lint test (3 cases).

Docs:
- skill-template/domains/mail.md: fix broken references (the two
  separate allowlist / feishu-native docs were merged into one
  lark-mail-html.md long ago); JSON example no longer mentions internal
  service names.
- skills/lark-mail/SKILL.md:63: section pointer now lands on a real
  references/lark-mail-html.md anchor instead of a phantom in-file
  section.
- 6 references/lark-mail-*.md: add --body-file row alongside --body.
Previous rgb(225,77,42) = #E14D2A has hue 11.5° which falls in the
red-orange boundary and looks almost identical to the 警示红 (h=1.7°)
in real-world mail-client rendering. Visual reviewer (用户在飞书 mail
客户端测 A23 调色盘 case) confirmed they could not perceive the orange
semantic.

Bump to rgb(255,140,40) = #FF8C28 (hue 30°, saturation 84%) which is
unambiguously orange and clearly distinct from 警示红 (h≈2°) and 紧急
橙 (h≈30°) now have ~28° hue spacing.

References:
- A23 V2 testcase 实测视觉反馈
- HSV 色相分析: standard orange hue is 20-50°
之前的示例把多级列表写成"独立 ol + 独立 ul + 独立 ol(接续编号)"
兄弟堆叠的形式,且内部 ul 直接套 ul(违反 HTML 规范要求 <ul>/<ol>
的直接子节点必须是 <li>)。

实测视觉效果(lcpr +send 发文档原样示例到飞书 mail 客户端):
- 编号 1 / 2 的子项不缩进于父项(独立列表跟编号项是兄弟)
- lint autofix 检测到 <ul><ul> 不合规,wrap 出空 <li class="bullet1">
  导致显示空圆点 marker("点后没文字"现象)

修正:合并 3 个独立 ol/ul 为单一 ol,子 ul 嵌套在父 <li> 内:
  <ol>
    <li>第一级
      <ul>
        <li>第二级
          <ul><li>第三级</li></ul>  ← 嵌套在父 li 内,不是兄弟
        </li>
      </ul>
    </li>
    <li>第一级接续编号</li>
  </ol>

同时去掉显式 start="1" / start="2" / data-start,同一 ol 内 li
顺序自动编号,无需手动指定。

注释里补充说明"<ul>/<ol> 的直接子节点必须是 <li>"以防再次误用。
之前修正后的示例混用 ol+ul(外层 ol,内层 ul/ul),读者不容易看清
"全 ol 多级"和"全 ul 多级"分别该怎么写。拆成两个独立示例:

- 示例 1:全 ol 三级(decimal → lower-alpha → lower-roman)
  class 用 number1/number2/number3 区分层级
- 示例 2:全 ul 三级(disc → circle → square)
  class 用 bullet1/bullet2/bullet3 区分层级

两个示例都遵循同样的嵌套规则(子列表放在父 <li> 内、子级 margin-left
24px 视觉缩进),通用规则单独提到注释顶部不重复。
之前两个模板把多级列表写成"独立 ol(start=N) + 独立 blockquote +
独立 <ul><li><ul>...</ul></li></ul> 三明治"的兄弟堆叠模式,结构上:
- 多个独立 ol 拆开导致编号靠 start="N" 硬编码续接,写错就乱
- 三明治外层 ul 内只有一个空 li 包子 ul(违反 HTML 规范 ul 不能直接套 ul)
- autofix wrap 出空 <li class="bullet1"> 显示默认圆点 marker("点后没文字"现象)

修正:每段合并成单 ol,子项 ul 嵌套在父 <li> 内,去掉 start="N" /
data-start="N"(同 ol 内 li 自动连续编号)。三级使用
list-style-type: decimal → lower-alpha → lower-roman 区分层级。

具体变动:
- weekly--team-report.html
  - 本周工作段:3 ol + 3 blockquote + 3 三明治 ul = 9 个兄弟块
    → 单 ol 含 3 li(事件 1/2/3),blockquote / 子项 ul / 孙子项 ul
    都嵌入父 li 内
  - 下周工作段:2 ol + 1 三明治 ol = 3 个兄弟块
    → 单 ol 含 4 li(重点 1/2/3/4),重点 2 li 内嵌 lower-alpha 子 ol
- job-application--resume.html
  - 工作经历 + 项目经历 段同模式重写(单 ol 含多 li 嵌套)
  - 顶部 cover letter 简化:3 行"尊敬的 xxx:/ 您好!/ 长正式介绍"
    → 2 行"[称呼],您好:/ 1 句直接介绍",符合标准中文求职邮件开头风格

验证:两个模板分别跑 lcpr +lint-html --show-lint-details:
- 关键结构 bug 0 个(无 LIST_DIRECT_CHILD_NON_LI 触发)
- cleaned_html 回写再 lint = 0/0 idempotent
- 剩余 STYLE_LIST_NATIVE_INLINE_APPLIED warning 是 lcpr 加 canonical
  属性顺序的 autofix 提示,跟 reference doc 自身列表示例同行为
将 spec §/S2 contract/KB Pitfall/KB conventions/technical-design/editor-kit/
renderer-side CSP/服务端 sanitizer 等内部引用全部改写为公开读者可理解的中立表述:
- spec §4.x 章节号:删除引用锚点,保留注释描述的规则
- S2 contract «XXX»:删除规范名和书名号,有价值的描述改为普通自然语言
- KB Pitfall N / KB conventions:删除知识库引用,保留实际技术说明
- technical-design §4.4:改为 three-tier tag classification
- editor-kit:改为 Feishu mail-editor's renderer
- renderer-side CSP:改为 the rendering layer's CSP
- 服务端 sanitizer:改为客户端兼容性与安全沙箱约束
1. 删除 lark-mail-lint-html.md 中不存在的 --auto-fix 和 --strict 两个 flag(参数表、示例命令、字段说明共 5 处),改成符合"autofix 始终启用"现状的描述
2. 将未知 URL scheme(webcal:// 等)的 lint finding 从 Applied(SeverityWarning)改为 Blocked(SeverityError)——行为是"删除属性",应与其他删除类 finding 语义一致;同步更新 linter_test.go 中对应测试(TestRun_UnknownSchemeWarning → TestRun_UnknownSchemeBlocked)
3. 删除 mail_send.go::resolveSendBody 和 mail_draft_create.go::resolveDraftCreateBody 两个与 body_file.go::resolveBodyFromFlags 完全等价的重复函数,调用点改为 resolveBodyFromFlags
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain/mail PR touches the mail domain size/XL Architecture-level or global-impact change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants