Skip to content

feat(mail): add template management shortcuts and --template-id flag#642

Open
infeng wants to merge 3 commits intolarksuite:mainfrom
infeng:harness/01kpmfk36nw9fhn08mab2sbjkb-v2
Open

feat(mail): add template management shortcuts and --template-id flag#642
infeng wants to merge 3 commits intolarksuite:mainfrom
infeng:harness/01kpmfk36nw9fhn08mab2sbjkb-v2

Conversation

@infeng
Copy link
Copy Markdown
Collaborator

@infeng infeng commented Apr 23, 2026

Summary

Adds email template management to lark-cli's +mail domain, rebased on top of latest main.

  • New shortcuts: mail +template-create, mail +template-update
  • New flag on existing shortcuts: --template-id on +send / +draft-create / +reply / +reply-all / +forward
  • HTML inline image handling: scan <img src> local paths → Drive upload → rewrite as cid: refs, emit via TemplateAttachment{id=file_key, is_inline=true, cid, ...}
  • Template does NOT carry IsSendSeparately — per-recipient separate-send is a draft-level concern and is intentionally not persisted on the template record.
  • Template addresses use the IDL's native mail_address field only (no address_alias fallback).
  • Key non-sensitive Info logs added at template-create / template-update / --template-id apply paths (mailbox_id, template_id, counts, name_len — never content / subject / recipient plaintext).

Relationship to earlier PR

Supersedes the earlier branch harness/01kpmfk36nw9fhn08mab2sbjkb — that branch was based on an old main and mixed unrelated in-flight mail work. This branch rebuilds only the template work on top of current upstream/main.

Test plan

  • go build ./... passes locally
  • mail +template-create --help surfaces the new flags
  • mail +send --template-id <id> DryRun shows extra GET step + merged fields
  • Inline image in template_content gets uploaded to Drive and CID-substituted

Summary by CodeRabbit

Release Notes

  • New Features
    • Added email template management: create and update personal email templates
    • Added --template-id support across send, draft, reply, reply-all, and forward operations
    • Added --subject override option for reply, reply-all, forward, and draft-create operations
    • Template composition includes recipient, body, and subject merging with HTML image upload and attachment handling

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

This PR introduces comprehensive email template support to the mail service. It adds mail +template-create and mail +template-update shortcuts for managing templates, extends existing mail shortcuts (draft-create, send, reply, reply-all, forward) with --template-id support for composing from templates, and includes template composition logic for HTML image rewriting, attachment management, and field merging.

Changes

Cohort / File(s) Summary
Template Management Shortcuts
shortcuts/mail/mail_template_create.go, shortcuts/mail/mail_template_update.go
New shortcuts for creating and updating mail templates; handle HTML <img src> path scanning/rewriting to CID references, file attachment uploads to Drive, template payload construction, and server submission with dry-run step visibility and execute-phase result reporting.
Template Composition Core
shortcuts/mail/template_compose.go
New module providing template fetch/merge logic, HTML image rewriting utilities, attachment classification (SMALL/LARGE), CID generation, Drive upload coordination, recipient/body/subject merge rules per shortcut kind, template warning emission, and large-attachment-ID encoding for EML headers.
Existing Shortcuts - Template Integration
shortcuts/mail/mail_draft_create.go, shortcuts/mail/mail_send.go, shortcuts/mail/mail_reply.go, shortcuts/mail/mail_reply_all.go, shortcuts/mail/mail_forward.go
Each shortcut now accepts optional --template-id (and subject override where applicable); validation suppresses required-field checks when template is provided; execution path conditionally fetches template, applies merge logic, adjusts plaintext mode, collects large-attachment IDs, and injects them into EML headers via draftpkg.LargeAttachmentIDsHeader; DryRun includes template-fetch steps.
Test Updates
shortcuts/mail/mail_draft_create_test.go
All buildRawEMLForDraftCreate call sites updated to pass additional nil trailing argument; no test logic or assertions changed.
Shortcuts Registry
shortcuts/mail/shortcuts.go
Shortcuts() function extended to include MailTemplateCreate and MailTemplateUpdate in returned slice.
Documentation
skills/lark-mail/SKILL.md, skills/lark-mail/references/lark-mail-templates.md
Extended SKILL.md with new shortcut flows and native API resources for template management; new guide documenting template create/update command flags, patch/flat-flag merging, inline image handling, attachment rules, and template usage with existing compose shortcuts including merge semantics and error codes.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI
    participant MailAPI
    participant DriveAPI
    participant Backend as Mail Server
    User->>CLI: mail +send --template-id ABC --body "custom"
    CLI->>CLI: Validate template ID format
    CLI->>MailAPI: Fetch template ABC
    MailAPI-->>CLI: Template (subject, body, to/cc/bcc, attachments)
    CLI->>DriveAPI: Upload files referenced in template
    DriveAPI-->>CLI: File keys for attachments
    CLI->>CLI: Merge template fields with user flags<br/>(user flags take precedence)
    CLI->>CLI: Rewrite HTML img src → cid: references
    CLI->>CLI: Classify attachments as SMALL/LARGE
    CLI->>Backend: POST draft with merged fields<br/>+ LargeAttachmentIDsHeader
    Backend-->>CLI: Draft created
    User->>CLI: mail +template-create --template-content-file tmpl.html<br/>--attach file1.pdf
    CLI->>CLI: Scan HTML for local img src paths
    CLI->>DriveAPI: Upload img and --attach files
    DriveAPI-->>CLI: File keys
    CLI->>CLI: Rewrite img src to cid: references<br/>Build attachment payload
    CLI->>MailAPI: POST mail.user_mailbox.templates.create
    MailAPI-->>CLI: Template created (id, attachments)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

domain/mail, size/XL

Suggested reviewers

  • chanthuang
  • hugang-lark

Poem

🐰 A rabbit hops through templates bright,
Creating, merging, rewriting in delight,
Images become CIDs, attachments find their way,
Draft-compose shortcuts bloom today! 📧✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: adding template management shortcuts and a new --template-id flag to mail shortcuts.
Description check ✅ Passed The description includes a summary, specific changes, and a test plan, but lacks unit test confirmation and manual verification checkboxes as required by the template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added domain/mail PR touches the mail domain size/XL Architecture-level or global-impact change labels Apr 23, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 23, 2026

Codecov Report

❌ Patch coverage is 4.09914% with 1006 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.37%. Comparing base (10f1f2e) to head (c93b4b9).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
shortcuts/mail/template_compose.go 2.11% 371 Missing ⚠️
shortcuts/mail/mail_template_update.go 0.00% 234 Missing ⚠️
shortcuts/mail/mail_template_create.go 0.00% 142 Missing ⚠️
shortcuts/mail/mail_draft_create.go 20.00% 53 Missing and 7 partials ⚠️
shortcuts/mail/mail_reply.go 10.34% 48 Missing and 4 partials ⚠️
shortcuts/mail/mail_forward.go 9.25% 45 Missing and 4 partials ⚠️
shortcuts/mail/mail_reply_all.go 10.90% 45 Missing and 4 partials ⚠️
shortcuts/mail/mail_send.go 6.00% 44 Missing and 3 partials ⚠️
shortcuts/mail/shortcuts.go 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #642      +/-   ##
==========================================
- Coverage   60.66%   59.37%   -1.29%     
==========================================
  Files         416      419       +3     
  Lines       43989    44985     +996     
==========================================
+ Hits        26686    26710      +24     
- Misses      15233    16188     +955     
- Partials     2070     2087      +17     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown

🚀 PR Preview Install Guide

🧰 CLI update

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

🧩 Skill update

npx skills add infeng/cli#harness/01kpmfk36nw9fhn08mab2sbjkb-v2 -y -g

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
shortcuts/mail/mail_forward.go (1)

374-383: ⚠️ Potential issue | 🔴 Critical

Two successive Header(LargeAttachmentIDsHeader, ...) calls will drop one set of IDs

Lines 379 and 382 both call bld.Header(draftpkg.LargeAttachmentIDsHeader, ...) with different values in immediate succession: first the forward-derived largeAttIDs (originals + user uploads), then the template-derived IDs. Unless emlbuilder.Header is strictly append-and-preserves-order and the server reads all values, one of the two header values is functionally discarded and large attachments from either the forwarded message/user uploads or the template will silently fail to reference from the draft.

Merge the two ID lists into a single base64-JSON value and emit Header(...) exactly once. This is the same underlying issue I flagged at mail_reply_all.go:265-267, mail_reply.go:255-257, and mail_send.go:216-218, but here it's most visible.

🐛 Emit a single merged header
-		if len(largeAttIDs) > 0 {
-			idsJSON, err := json.Marshal(largeAttIDs)
-			if err != nil {
-				return fmt.Errorf("failed to encode large attachment IDs: %w", err)
-			}
-			bld = bld.Header(draftpkg.LargeAttachmentIDsHeader, base64.StdEncoding.EncodeToString(idsJSON))
-		}
-		if hdr, hdrErr := encodeTemplateLargeAttachmentHeader(templateLargeAttachmentIDs); hdrErr == nil && hdr != "" {
-			bld = bld.Header(draftpkg.LargeAttachmentIDsHeader, hdr)
+		// Merge forward-derived and template-derived large attachment IDs into one header.
+		for _, id := range templateLargeAttachmentIDs {
+			if id != "" {
+				largeAttIDs = append(largeAttIDs, largeAttID{ID: id})
+			}
+		}
+		if len(largeAttIDs) > 0 {
+			idsJSON, err := json.Marshal(largeAttIDs)
+			if err != nil {
+				return fmt.Errorf("failed to encode large attachment IDs: %w", err)
+			}
+			bld = bld.Header(draftpkg.LargeAttachmentIDsHeader, base64.StdEncoding.EncodeToString(idsJSON))
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/mail_forward.go` around lines 374 - 383, Merge the two
separate header writes so the draft gets a single LargeAttachmentIDsHeader:
collect IDs from largeAttIDs and from
encodeTemplateLargeAttachmentHeader(templateLargeAttachmentIDs) (decode the
template header if needed or get its slice), combine them (dedupe if
appropriate), json.Marshal the combined slice and
base64.StdEncoding.EncodeToString the result, then call
bld.Header(draftpkg.LargeAttachmentIDsHeader, <encodedCombined>) exactly once;
keep existing error handling for json.Marshal and for
encodeTemplateLargeAttachmentHeader, and ensure you only set the header when the
final combined list is non-empty.
shortcuts/mail/mail_draft_create.go (1)

40-82: ⚠️ Potential issue | 🔴 Critical

Remove Required: true from conditionally-required flags to allow --template-id-only flows.

subject and body are declared Required: true in mail_draft_create.go (lines 40–41), yet the Validate function (lines 77–82) attempts to make them optional when --template-id is provided. Since Cobra enforces required flags at parse time before Validate runs, the conditional logic is ineffective—users invoking +draft-create --template-id <id> alone will fail with "required flags not set" before reaching the custom validator.

The same conflict exists in +send (subject/body Required), +reply (body Required), and +reply-all (body Required). Only +forward avoids this issue because body is not required. No test coverage was found for the --template-id feature, confirming this code path has not been exercised.

Fix: Drop Required: true from subject/body and rely on Validate for conditional enforcement (already in place for draft-create; add it to send/reply/reply-all). Update flag descriptions to note that flags are required unless --template-id is used.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/mail_draft_create.go` around lines 40 - 82, The subject/body
flags are marked Required:true but your command-level Validate logic
(validateTemplateID + the Validate function) already conditionally allows them
when --template-id is given; remove Required:true from the subject and body flag
declarations in the mail draft/create command (and likewise drop Required:true
for subject/body on +send and body on +reply and +reply-all), update their Desc
strings to note "required unless --template-id is used", and ensure each command
has a Validate method that enforces presence of subject/body when template-id is
empty (use the existing validateTemplateID and the same TrimSpace checks used in
mail_draft_create.go's Validate to make the enforcement consistent).
🧹 Nitpick comments (7)
shortcuts/mail/mail_reply.go (1)

150-152: Fragile subject round-trip through orig.subject

Reassigning orig.subject = s so that a later buildReplySubject(orig.subject) picks up the template subject works only as long as buildReplySubject is idempotent on "Re: …" prefixes (which is why the s != buildReplySubject(orig.subject) guard exists). A cleaner and harder-to-break approach is to compute the final subject directly from merged.Subject, matching how mail_send.go already consumes it. See the parallel comment on template_compose.go:499-584 about making subject handling consistent across the five shortcuts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/mail_reply.go` around lines 150 - 152, The current code
mutates orig.subject from merged.Subject and relies on
buildReplySubject(orig.subject) being idempotent; instead compute the final
subject directly from merged.Subject to avoid the fragile round-trip. Replace
the orig.subject reassignment and its comparison with
buildReplySubject(orig.subject) by computing finalSub :=
buildReplySubject(strings.TrimSpace(merged.Subject)) (or equivalent logic used
in mail_send.go/template_compose.go) and use finalSub for downstream processing;
reference symbols: merged.Subject, orig.subject, buildReplySubject, and ensure
behavior matches mail_send.go subject handling.
shortcuts/mail/template_compose.go (3)

64-71: Prefer sort.Strings over hand-rolled insertion sort

The justification in the comment ("to avoid importing sort in hot template path") doesn't hold — sort is already pulled into the binary via many transitive imports, and logTemplateInfo runs at most a few times per command invocation, not on a hot path. Using the standard library here avoids a custom implementation and an extra function on the review surface.

♻️ Use `sort.Strings`
 import (
 	"context"
 	"encoding/base64"
 	"encoding/json"
 	"fmt"
 	"net/url"
 	"path/filepath"
 	"regexp"
+	"sort"
 	"strconv"
 	"strings"
@@
-	// Stable key order so log lines are diff-friendly.
-	sortStrings(keys)
+	// Stable key order so log lines are diff-friendly.
+	sort.Strings(keys)
@@
-func sortStrings(s []string) {
-	// tiny insertion sort to avoid importing sort in hot template path.
-	for i := 1; i < len(s); i++ {
-		for j := i; j > 0 && s[j-1] > s[j]; j-- {
-			s[j-1], s[j] = s[j], s[j-1]
-		}
-	}
-}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/template_compose.go` around lines 64 - 71, Replace the custom
insertion sort implementation in sortStrings with the standard library call
sort.Strings to reduce custom code and surface area; import "sort" and change
the body of sortStrings to call sort.Strings(s) (this function is used in the
template path such as logTemplateInfo), ensuring the import is added and
removing the hand-rolled loop.

646-655: Dead conditional branch in mergeTemplateBody

The if draftpkg.HTMLContainsLargeAttachment(draftBody) { // fall through … } block has an empty body and no else, so the check is a no-op — the call result is discarded. Either remove the block or implement the intended behavior.

♻️ Remove the dead branch
-		// Regex replace: if the draft body already contains a quote block
-		// (some callers pre-compose it), insert template before it.
-		if draftpkg.HTMLContainsLargeAttachment(draftBody) {
-			// fall through — no quote heuristic; appending is safe.
-		}
 		merged := draftpkg.InsertBeforeQuoteOrAppend(draftBody, tplContent)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/template_compose.go` around lines 646 - 655, The empty
conditional using draftpkg.HTMLContainsLargeAttachment(draftBody) is a no-op and
should be removed; delete the whole if block and its comment so the code simply
calls merged := draftpkg.InsertBeforeQuoteOrAppend(draftBody, tplContent) and
returns merged or tplContent+draftBody as currently written, or alternatively,
if the original intent was special handling when a large attachment exists,
implement that logic inside mergeTemplateBody referencing
draftpkg.HTMLContainsLargeAttachment, draftpkg.InsertBeforeQuoteOrAppend,
draftBody and tplContent instead of leaving an empty branch.

365-388: Dead maps: pathToFileKey / pathToSize populated but never read

Only pathToCID is ever consulted for deduplication; the other two maps are written to but never queried. Either delete them or use them to skip re-uploads of duplicate paths (the current code calls uploadToDriveForTemplate again on the first hit only because pathToCID short-circuits before reaching the upload, so a rewrite of a different RawSrc pointing to the same Path would still short-circuit correctly — the file_key/size tracking is redundant).

♻️ Drop the unused maps
 	imgs := parseLocalImgs(content)
 	pathToCID := make(map[string]string)
-	pathToFileKey := make(map[string]string)
-	pathToSize := make(map[string]int64)
 	for _, img := range imgs {
@@
 		pathToCID[img.Path] = cid
-		pathToFileKey[img.Path] = fileKey
-		pathToSize[img.Path] = sz
 		content = replaceImgSrcOnce(content, img.RawSrc, "cid:"+cid)
 		builder.append(fileKey, filepath.Base(img.Path), cid, true, sz)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/template_compose.go` around lines 365 - 388, The maps
pathToFileKey and pathToSize are populated but never read; remove their
declarations and all assignments to them (the make(...) lines and the lines
setting pathToFileKey[img.Path] = fileKey and pathToSize[img.Path] = sz) to
eliminate dead state in the parseLocalImgs loop where pathToCID,
uploadToDriveForTemplate, generateTemplateCID, replaceImgSrcOnce, and
builder.append are used; alternatively, if you prefer to avoid re-uploads
instead, check pathToFileKey[path] (or pathToSize) before calling
uploadToDriveForTemplate and reuse the saved fileKey/sz when present—choose one
approach and remove the unused code for the other.
shortcuts/mail/mail_template_create.go (1)

84-84: Consider validating that a usable content source exists when inline attachments are expected

Validate does not require either --template-content or --template-content-file, which is fine for recipient-only templates. However, nothing catches the case where the user passes only --template-content-file=<bad-path> — that error surfaces as an ugly runtime failure during Execute's first call to resolveTemplateContent rather than during validation. Consider a quick runtime.FileIO().Stat(...) in Validate when --template-content-file is set, so errors appear before any Drive work begins.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/mail_template_create.go` at line 84, Update the Validate
method to check that when TemplateContentFile (the --template-content-file flag)
is provided it points to a readable file: call runtime.FileIO().Stat(...) (or
equivalent file-existence/readability check) inside Validate and return a
descriptive validation error if Stat fails, so malformed paths are caught during
Validate rather than later in Execute/resolveTemplateContent; keep existing
behavior allowing neither template-content nor template-content-file when
recipient-only templates are intended.
shortcuts/mail/mail_template_update.go (2)

163-178: Prefer defer f.Close() for the patch file.

f.Close() is called manually after io.ReadAll. If a future change introduces an early return between Open and Close (or ReadAll panics), the file handle leaks. defer is the idiomatic guard.

♻️ Proposed refactor
 		if pf := strings.TrimSpace(runtime.Str("patch-file")); pf != "" {
 			f, err := runtime.FileIO().Open(pf)
 			if err != nil {
 				return output.ErrValidation("open --patch-file %s: %v", pf, err)
 			}
-			buf, readErr := io.ReadAll(f)
-			f.Close()
+			defer f.Close()
+			buf, readErr := io.ReadAll(f)
 			if readErr != nil {
 				return output.ErrValidation("read --patch-file %s: %v", pf, readErr)
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/mail_template_update.go` around lines 163 - 178, Open file
handles with immediate defer to ensure they are closed on all returns: after
calling runtime.FileIO().Open(pf) in the block that reads the patch file, check
err as before and then call defer f.Close() (instead of the current manual
f.Close() after io.ReadAll); keep the existing io.ReadAll, json.Unmarshal into
templatePatchFile, and applyTemplatePatchFile(tpl, &patch) logic unchanged, and
optionally handle or ignore the error returned by Close to avoid leaking the
file descriptor if later returns or panics occur.

60-66: Dry-run silently swallows --set-template-content-file errors.

resolveTemplateUpdateContent can return an error (file missing/unreadable), but the dry-run discards it with _, _, _. If the file path is wrong, the resulting preview will show no upload steps for inline images and the user will only discover the issue during Execute. Consider surfacing the error as a dry-run hint so the preview is predictable.

♻️ Proposed refactor
-		content, _, _ := resolveTemplateUpdateContent(runtime)
+		content, _, contentErr := resolveTemplateUpdateContent(runtime)
+		if contentErr != nil {
+			api = api.Set("warning", fmt.Sprintf("could not read --set-template-content-file: %v", contentErr))
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/mail_template_update.go` around lines 60 - 66,
resolveTemplateUpdateContent errors are being ignored in the dry-run path
(content, _, _ := resolveTemplateUpdateContent(runtime)), which causes previews
to miss upload steps for invalid template file paths; change this to capture the
error (e.g., content, _, err := resolveTemplateUpdateContent(runtime)) and, when
err != nil during dry-run, surface it as a preview hint or warning (or return
the error) so users see a clear message before Execute; update the block that
calls parseLocalImgs, addTemplateUploadSteps, splitByComma and
runtime.Str("attach") to handle and propagate/report the captured error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@shortcuts/mail/mail_template_create.go`:
- Around line 86-148: The new +template-create shortcut (Execute function) lacks
unit and dry-run tests: add tests that exercise resolveTemplateContent and
buildTemplatePayloadFromFlags behavior — specifically validate --name length
rejection, mutual exclusion of --template-content vs --template-content-file
(including file-read error propagation from resolveTemplateContent), and the
inline-image extraction + Drive-upload flow (mock Drive HTTP calls via
httpmock); also include a DryRun/E2E test mirroring other mail shortcuts that
invokes Execute in dry-run mode and asserts the runtime output and that
createTemplate is not called / is invoked as expected (mock
createTemplate/extractTemplatePayload where appropriate).
- Around line 36-73: The DryRun handler silently discards errors from
resolveTemplateContent (content, _, _ := resolveTemplateContent(runtime)),
causing misleading dry-run counts; change the call in the DryRun function to
capture the error (e.g. content, _, err := resolveTemplateContent(runtime)) and
if err != nil log a warning via the runtime/context logger (using
logTemplateInfo or runtime.Logger if available) that includes the error message
and short context (e.g. "template content load failed"), then proceed but ensure
counts are computed from the (possibly empty) content while the warning surfaces
the problem to the user; update any references in the same block
(attachments_total, inline_count) to use the captured content variable.

In `@shortcuts/mail/template_compose.go`:
- Around line 499-584: applyTemplate sets res.Subject but reply-all and forward
currently ignore it (they read runtime.Str("subject") into subjectOverride),
causing template subjects to be dropped; make behavior consistent by having
reply_all and forward consume the merged subject from applyTemplate unless the
user explicitly passed --subject. Concretely, in mail_reply_all.go and
mail_forward.go, replace the current subjectOverride usage of
runtime.Str("subject") with logic that prefers the applyTemplate result's
Subject (template-merged value) and only uses the runtime flag when non-empty
(matching the precedence implemented by applyTemplate); you can mirror the
approach used by mail_send.go and/or the round-trip in mail_reply.go so all
shortcuts use the merged.Subject from applyTemplate when userSubject is not
provided.
- Around line 576-584: The code in applyTemplate is collecting all attachment
IDs into the LargeAttachmentIDs slice regardless of attachment type; update the
loop that iterates tpl.Attachments to only append att.ID when att.AttachmentType
(or equivalent enum/field) indicates LARGE, so LargeAttachmentIDs truly contains
only large attachments; keep the LargeAttachmentIDs field name and callers
(encodeTemplateLargeAttachmentHeader in mail_send.go, mail_reply.go,
mail_reply_all.go, mail_forward.go) unchanged so headers written match what
decodeLargeAttachmentHeader expects.
- Around line 604-658: mergeTemplateBody correctly injects tpl.TemplateContent
(with <img src="cid:...">) but applyTemplate and templateApplyResult only
propagate LargeAttachmentIDs and drop inline attachment metadata, so inline
images never get added via AddFileInline and render broken; to fix, extend
templateApplyResult to include inline attachment info (e.g., struct fields for
IsInline, CID, and file path/file_key), update applyTemplate to populate those
inline entries for both LARGE and SMALL template attachments, and then update
the downstream send/draft code paths (where EML is built and headers like
X-Lms-Large-Attachment-Ids are set) to iterate over
templateApplyResult.inlineAttachments and call AddFileInline for each before
constructing/sending the EML so cid: references are satisfied.

In `@skills/lark-mail/references/lark-mail-templates.md`:
- Around line 115-117: The fenced code block containing the warning string
"warning: template to/cc/bcc are appended without de-duplication; you may see
repeated recipients. Use --to/--cc/--bcc to override, or run +template-update to
clear template addresses." needs a language tag to satisfy markdownlint MD040;
update the triple-backtick fence that surrounds that warning to use a language
like text (e.g., ```text) so renderers and linters pick sensible defaults.
- Line 39: The doc states the LARGE threshold as 25 MB but the implementation in
templateAttachmentBuilder.append uses 20*1024*1024; update the markdown to match
the code by changing both occurrences of "25 MB" to "20 MB" (so the description
of switching and the Q4 line read 20 MB) to reflect the logic in
templateAttachmentBuilder.append in template_compose.go where b.projectedSize >
20*1024*1024 sets b.largeBucket = true.

---

Outside diff comments:
In `@shortcuts/mail/mail_draft_create.go`:
- Around line 40-82: The subject/body flags are marked Required:true but your
command-level Validate logic (validateTemplateID + the Validate function)
already conditionally allows them when --template-id is given; remove
Required:true from the subject and body flag declarations in the mail
draft/create command (and likewise drop Required:true for subject/body on +send
and body on +reply and +reply-all), update their Desc strings to note "required
unless --template-id is used", and ensure each command has a Validate method
that enforces presence of subject/body when template-id is empty (use the
existing validateTemplateID and the same TrimSpace checks used in
mail_draft_create.go's Validate to make the enforcement consistent).

In `@shortcuts/mail/mail_forward.go`:
- Around line 374-383: Merge the two separate header writes so the draft gets a
single LargeAttachmentIDsHeader: collect IDs from largeAttIDs and from
encodeTemplateLargeAttachmentHeader(templateLargeAttachmentIDs) (decode the
template header if needed or get its slice), combine them (dedupe if
appropriate), json.Marshal the combined slice and
base64.StdEncoding.EncodeToString the result, then call
bld.Header(draftpkg.LargeAttachmentIDsHeader, <encodedCombined>) exactly once;
keep existing error handling for json.Marshal and for
encodeTemplateLargeAttachmentHeader, and ensure you only set the header when the
final combined list is non-empty.

---

Nitpick comments:
In `@shortcuts/mail/mail_reply.go`:
- Around line 150-152: The current code mutates orig.subject from merged.Subject
and relies on buildReplySubject(orig.subject) being idempotent; instead compute
the final subject directly from merged.Subject to avoid the fragile round-trip.
Replace the orig.subject reassignment and its comparison with
buildReplySubject(orig.subject) by computing finalSub :=
buildReplySubject(strings.TrimSpace(merged.Subject)) (or equivalent logic used
in mail_send.go/template_compose.go) and use finalSub for downstream processing;
reference symbols: merged.Subject, orig.subject, buildReplySubject, and ensure
behavior matches mail_send.go subject handling.

In `@shortcuts/mail/mail_template_create.go`:
- Line 84: Update the Validate method to check that when TemplateContentFile
(the --template-content-file flag) is provided it points to a readable file:
call runtime.FileIO().Stat(...) (or equivalent file-existence/readability check)
inside Validate and return a descriptive validation error if Stat fails, so
malformed paths are caught during Validate rather than later in
Execute/resolveTemplateContent; keep existing behavior allowing neither
template-content nor template-content-file when recipient-only templates are
intended.

In `@shortcuts/mail/mail_template_update.go`:
- Around line 163-178: Open file handles with immediate defer to ensure they are
closed on all returns: after calling runtime.FileIO().Open(pf) in the block that
reads the patch file, check err as before and then call defer f.Close() (instead
of the current manual f.Close() after io.ReadAll); keep the existing io.ReadAll,
json.Unmarshal into templatePatchFile, and applyTemplatePatchFile(tpl, &patch)
logic unchanged, and optionally handle or ignore the error returned by Close to
avoid leaking the file descriptor if later returns or panics occur.
- Around line 60-66: resolveTemplateUpdateContent errors are being ignored in
the dry-run path (content, _, _ := resolveTemplateUpdateContent(runtime)), which
causes previews to miss upload steps for invalid template file paths; change
this to capture the error (e.g., content, _, err :=
resolveTemplateUpdateContent(runtime)) and, when err != nil during dry-run,
surface it as a preview hint or warning (or return the error) so users see a
clear message before Execute; update the block that calls parseLocalImgs,
addTemplateUploadSteps, splitByComma and runtime.Str("attach") to handle and
propagate/report the captured error.

In `@shortcuts/mail/template_compose.go`:
- Around line 64-71: Replace the custom insertion sort implementation in
sortStrings with the standard library call sort.Strings to reduce custom code
and surface area; import "sort" and change the body of sortStrings to call
sort.Strings(s) (this function is used in the template path such as
logTemplateInfo), ensuring the import is added and removing the hand-rolled
loop.
- Around line 646-655: The empty conditional using
draftpkg.HTMLContainsLargeAttachment(draftBody) is a no-op and should be
removed; delete the whole if block and its comment so the code simply calls
merged := draftpkg.InsertBeforeQuoteOrAppend(draftBody, tplContent) and returns
merged or tplContent+draftBody as currently written, or alternatively, if the
original intent was special handling when a large attachment exists, implement
that logic inside mergeTemplateBody referencing
draftpkg.HTMLContainsLargeAttachment, draftpkg.InsertBeforeQuoteOrAppend,
draftBody and tplContent instead of leaving an empty branch.
- Around line 365-388: The maps pathToFileKey and pathToSize are populated but
never read; remove their declarations and all assignments to them (the make(...)
lines and the lines setting pathToFileKey[img.Path] = fileKey and
pathToSize[img.Path] = sz) to eliminate dead state in the parseLocalImgs loop
where pathToCID, uploadToDriveForTemplate, generateTemplateCID,
replaceImgSrcOnce, and builder.append are used; alternatively, if you prefer to
avoid re-uploads instead, check pathToFileKey[path] (or pathToSize) before
calling uploadToDriveForTemplate and reuse the saved fileKey/sz when
present—choose one approach and remove the unused code for the other.
🪄 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: c0b839cf-23e7-4df6-b934-e4a918262ac2

📥 Commits

Reviewing files that changed from the base of the PR and between 10f1f2e and c93b4b9.

📒 Files selected for processing (12)
  • shortcuts/mail/mail_draft_create.go
  • shortcuts/mail/mail_draft_create_test.go
  • shortcuts/mail/mail_forward.go
  • shortcuts/mail/mail_reply.go
  • shortcuts/mail/mail_reply_all.go
  • shortcuts/mail/mail_send.go
  • shortcuts/mail/mail_template_create.go
  • shortcuts/mail/mail_template_update.go
  • shortcuts/mail/shortcuts.go
  • shortcuts/mail/template_compose.go
  • skills/lark-mail/SKILL.md
  • skills/lark-mail/references/lark-mail-templates.md

Comment on lines +36 to +73
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
mailboxID := resolveComposeMailboxID(runtime)
content, _, _ := resolveTemplateContent(runtime)
logTemplateInfo(runtime, "create.dry_run", map[string]interface{}{
"mailbox_id": mailboxID,
"is_plain_text_mode": runtime.Bool("plain-text"),
"name_len": len([]rune(runtime.Str("name"))),
"attachments_total": len(splitByComma(runtime.Str("attach"))) + len(parseLocalImgs(content)),
"inline_count": len(parseLocalImgs(content)),
"tos_count": countAddresses(runtime.Str("to")),
"ccs_count": countAddresses(runtime.Str("cc")),
"bccs_count": countAddresses(runtime.Str("bcc")),
})
api := common.NewDryRunAPI().
Desc("Create a new mail template. The command scans HTML for local <img src> references, uploads each inline image to Drive (≤20MB single upload_all; >20MB upload_prepare+upload_part+upload_finish), rewrites <img src> values to cid: references, uploads any non-inline --attach files the same way, and finally POSTs a Template payload to mail.user_mailbox.templates.create.")
// Surface the Drive upload steps explicitly so AI callers see the
// chunked vs single-part branch point for each local image.
for _, img := range parseLocalImgs(content) {
addTemplateUploadSteps(runtime, api, img.Path)
}
for _, p := range splitByComma(runtime.Str("attach")) {
addTemplateUploadSteps(runtime, api, p)
}
api = api.POST(templateMailboxPath(mailboxID)).
Body(map[string]interface{}{
"template": map[string]interface{}{
"name": runtime.Str("name"),
"subject": runtime.Str("subject"),
"template_content": "<rewritten-HTML-or-text>",
"is_plain_text_mode": runtime.Bool("plain-text"),
"tos": renderTemplateAddresses(runtime.Str("to")),
"ccs": renderTemplateAddresses(runtime.Str("cc")),
"bccs": renderTemplateAddresses(runtime.Str("bcc")),
"attachments": "<computed from uploads>",
},
})
return api
},
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

DryRun silently swallows resolveTemplateContent error

If --template-content-file is set but unreadable, content, _, _ := resolveTemplateContent(runtime) hides the error, and the DryRun output happily reports attachments_total/inline_count based on an empty body. Surface the error so mail +template-create --dry-run --template-content-file nonexistent.html still signals a problem to the user.

🛠️ Propagate the error via a warning log
-		content, _, _ := resolveTemplateContent(runtime)
+		content, _, rcErr := resolveTemplateContent(runtime)
+		if rcErr != nil {
+			fmt.Fprintf(runtime.IO().ErrOut, "warning: dry-run could not load template content: %v\n", rcErr)
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/mail_template_create.go` around lines 36 - 73, The DryRun
handler silently discards errors from resolveTemplateContent (content, _, _ :=
resolveTemplateContent(runtime)), causing misleading dry-run counts; change the
call in the DryRun function to capture the error (e.g. content, _, err :=
resolveTemplateContent(runtime)) and if err != nil log a warning via the
runtime/context logger (using logTemplateInfo or runtime.Logger if available)
that includes the error message and short context (e.g. "template content load
failed"), then proceed but ensure counts are computed from the (possibly empty)
content while the warning surfaces the problem to the user; update any
references in the same block (attachments_total, inline_count) to use the
captured content variable.

Comment on lines +86 to +148
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
mailboxID := resolveComposeMailboxID(runtime)
content, _, err := resolveTemplateContent(runtime)
if err != nil {
return err
}
name := runtime.Str("name")
subject := runtime.Str("subject")
isPlainText := runtime.Bool("plain-text")
tos := renderTemplateAddresses(runtime.Str("to"))
ccs := renderTemplateAddresses(runtime.Str("cc"))
bccs := renderTemplateAddresses(runtime.Str("bcc"))

rewritten, atts, err := buildTemplatePayloadFromFlags(
ctx, runtime, name, subject, content, tos, ccs, bccs,
splitByComma(runtime.Str("attach")),
)
if err != nil {
return err
}
inlineCount, largeCount := countAttachmentsByType(atts)
logTemplateInfo(runtime, "create.execute", map[string]interface{}{
"mailbox_id": mailboxID,
"is_plain_text_mode": isPlainText,
"name_len": len([]rune(name)),
"attachments_total": len(atts),
"inline_count": inlineCount,
"large_count": largeCount,
"tos_count": len(tos),
"ccs_count": len(ccs),
"bccs_count": len(bccs),
})

payload := &templatePayload{
Name: name,
Subject: subject,
TemplateContent: rewritten,
IsPlainTextMode: isPlainText,
Tos: tos,
Ccs: ccs,
Bccs: bccs,
Attachments: atts,
}

resp, err := createTemplate(runtime, mailboxID, payload)
if err != nil {
return fmt.Errorf("create template failed: %w", err)
}
tpl, _ := extractTemplatePayload(resp)
out := map[string]interface{}{
"template": tpl,
}
runtime.OutFormat(out, nil, func(w io.Writer) {
fmt.Fprintln(w, "Template created.")
if tpl != nil {
fmt.Fprintf(w, "template_id: %s\n", tpl.TemplateID)
fmt.Fprintf(w, "name: %s\n", tpl.Name)
fmt.Fprintf(w, "attachments: %d\n", len(tpl.Attachments))
}
})
return nil
},
}
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

Missing tests for the new +template-create shortcut

Per the repo coding guidelines ("Every behavior change must have an accompanying test"), this new shortcut should ship with at least one unit/DryRun test covering: --name length validation, the --template-content vs --template-content-file mutual-exclusion rule, the content-file read path (including the error-propagation behavior noted above), and the inline-image extraction/Drive-upload flow (mockable via httpmock). Adjacent dry-run E2E coverage under tests/cli_e2e/dryrun/ would also match the pattern used by other mail shortcuts.

As per coding guidelines: **/*.go: "Every behavior change must have an accompanying test".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/mail_template_create.go` around lines 86 - 148, The new
+template-create shortcut (Execute function) lacks unit and dry-run tests: add
tests that exercise resolveTemplateContent and buildTemplatePayloadFromFlags
behavior — specifically validate --name length rejection, mutual exclusion of
--template-content vs --template-content-file (including file-read error
propagation from resolveTemplateContent), and the inline-image extraction +
Drive-upload flow (mock Drive HTTP calls via httpmock); also include a
DryRun/E2E test mirroring other mail shortcuts that invokes Execute in dry-run
mode and asserts the runtime output and that createTemplate is not called / is
invoked as expected (mock createTemplate/extractTemplatePayload where
appropriate).

Comment on lines +499 to +584
// applyTemplate merges a fetched template with draft-derived and user-flag
// values. draftTo/Cc/Bcc are the addresses already on the draft (from the
// original message for reply/reply-all/forward, or the user flags for send/
// draft-create). userTo/Cc/Bcc/Subject/Body are user-supplied flag values
// (empty string = not provided).
func applyTemplate(
kind templateShortcutKind,
tpl *templatePayload,
draftTo, draftCc, draftBcc string,
draftSubject string,
draftBody string,
userTo, userCc, userBcc, userSubject, userBody string,
) templateApplyResult {
res := templateApplyResult{}

// Start with whatever is already in the draft (or the user-explicit
// draft-to values for send/draft-create).
effTo := draftTo
effCc := draftCc
effBcc := draftBcc
// User-flag --to/--cc/--bcc values override draft-derived values
// before template injection.
if userTo != "" {
effTo = userTo
}
if userCc != "" {
effCc = userCc
}
if userBcc != "" {
effBcc = userBcc
}

tplTo := joinTemplateAddresses(tpl.Tos)
tplCc := joinTemplateAddresses(tpl.Ccs)
tplBcc := joinTemplateAddresses(tpl.Bccs)

// Append template to/cc/bcc into draft to/cc/bcc.
effTo = appendAddrList(effTo, tplTo)
effCc = appendAddrList(effCc, tplCc)
effBcc = appendAddrList(effBcc, tplBcc)

res.To = effTo
res.Cc = effCc
res.Bcc = effBcc

// Q2: subject merging. User --subject wins, else draft non-empty wins,
// else template subject.
switch {
case strings.TrimSpace(userSubject) != "":
res.Subject = userSubject
case strings.TrimSpace(draftSubject) != "":
res.Subject = draftSubject
default:
res.Subject = tpl.Subject
}

// Q3: body merging. The shortcut-specific HTML/plain-text injection is
// handled by the caller; applyTemplate returns a merged body string that
// the caller can feed back into its compose pipeline.
res.Body = mergeTemplateBody(kind, tpl, draftBody, userBody)

// IsPlainTextMode propagation: template value wins.
res.IsPlainTextMode = tpl.IsPlainTextMode

// Q4: warn when reply / reply-all + template has to/cc/bcc (likely
// duplicates against the reply-derived recipients).
if (kind == templateShortcutReply || kind == templateShortcutReplyAll) &&
(len(tpl.Tos) > 0 || len(tpl.Ccs) > 0 || len(tpl.Bccs) > 0) {
res.Warnings = append(res.Warnings,
"template to/cc/bcc are appended without de-duplication; "+
"you may see repeated recipients. Use --to/--cc/--bcc to override, "+
"or run +template-update to clear template addresses.")
}

// Collect template attachment ids for the caller (file_keys). The SEND
// path uses these as the X-Lms-Large-Attachment-Ids header entries for
// LARGE types; SMALL entries are reused by file_key server-side.
for _, att := range tpl.Attachments {
if att.ID == "" {
continue
}
res.LargeAttachmentIDs = append(res.LargeAttachmentIDs, att.ID)
}

return res
}
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

merged.Subject is silently discarded in reply-all and forward shortcuts

applyTemplate computes res.Subject with a documented precedence (user subject → draft subject → template subject), but:

  • mail_send.go uses merged.Subject (good).
  • mail_reply.go applies it via a round-trip through orig.subject (works but awkward).
  • mail_reply_all.go and mail_forward.go never read merged.Subject; they only honor runtime.Str("subject") through subjectOverride. When a template has a non-empty subject and the user did not pass --subject, the template subject is dropped.

Please make the behavior consistent across all five shortcuts (either always consume merged.Subject, or document that reply/reply-all/forward intentionally keep the Re:/Fw: auto-subject and only honor --subject). If the latter, res.Subject in applyTemplate is misleading and should be scoped accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/template_compose.go` around lines 499 - 584, applyTemplate
sets res.Subject but reply-all and forward currently ignore it (they read
runtime.Str("subject") into subjectOverride), causing template subjects to be
dropped; make behavior consistent by having reply_all and forward consume the
merged subject from applyTemplate unless the user explicitly passed --subject.
Concretely, in mail_reply_all.go and mail_forward.go, replace the current
subjectOverride usage of runtime.Str("subject") with logic that prefers the
applyTemplate result's Subject (template-merged value) and only uses the runtime
flag when non-empty (matching the precedence implemented by applyTemplate); you
can mirror the approach used by mail_send.go and/or the round-trip in
mail_reply.go so all shortcuts use the merged.Subject from applyTemplate when
userSubject is not provided.

Comment on lines +576 to +584
for _, att := range tpl.Attachments {
if att.ID == "" {
continue
}
res.LargeAttachmentIDs = append(res.LargeAttachmentIDs, att.ID)
}

return res
}
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 | 🟠 Major

Header likely to include SMALL template attachments, not just LARGE

applyTemplate aggregates every attachment ID regardless of AttachmentType, and the callers in mail_send.go, mail_reply.go, mail_reply_all.go, mail_forward.go feed the resulting slice into encodeTemplateLargeAttachmentHeader, which is then written as X-Lms-Large-Attachment-Ids on the outgoing EML. Based on the header name and the server-side decoder (decodeLargeAttachmentHeader in shortcuts/mail/draft/large_attachment_parse.go), that header is specifically for LARGE attachments — feeding SMALL file_keys into it is likely to cause the server to mis-classify all template attachments as large, render a download-card for SMALL items, or reject the entries entirely.

The field name LargeAttachmentIDs also advertises large-only semantics while the code collects all IDs, so either the filter or the field should be tightened.

🛠️ Filter to LARGE-only when aggregating template attachment IDs for the header
-	for _, att := range tpl.Attachments {
-		if att.ID == "" {
-			continue
-		}
-		res.LargeAttachmentIDs = append(res.LargeAttachmentIDs, att.ID)
-	}
+	for _, att := range tpl.Attachments {
+		if att.ID == "" || att.AttachmentType != attachmentTypeLARGE {
+			continue
+		}
+		res.LargeAttachmentIDs = append(res.LargeAttachmentIDs, att.ID)
+	}
📝 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
for _, att := range tpl.Attachments {
if att.ID == "" {
continue
}
res.LargeAttachmentIDs = append(res.LargeAttachmentIDs, att.ID)
}
return res
}
for _, att := range tpl.Attachments {
if att.ID == "" || att.AttachmentType != attachmentTypeLARGE {
continue
}
res.LargeAttachmentIDs = append(res.LargeAttachmentIDs, att.ID)
}
return res
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/template_compose.go` around lines 576 - 584, The code in
applyTemplate is collecting all attachment IDs into the LargeAttachmentIDs slice
regardless of attachment type; update the loop that iterates tpl.Attachments to
only append att.ID when att.AttachmentType (or equivalent enum/field) indicates
LARGE, so LargeAttachmentIDs truly contains only large attachments; keep the
LargeAttachmentIDs field name and callers (encodeTemplateLargeAttachmentHeader
in mail_send.go, mail_reply.go, mail_reply_all.go, mail_forward.go) unchanged so
headers written match what decodeLargeAttachmentHeader expects.

Comment on lines +604 to +658
func mergeTemplateBody(kind templateShortcutKind, tpl *templatePayload, draftBody, userBody string) string {
tplContent := tpl.TemplateContent
// If the user explicitly passed --body, that is the composer's own
// authoring area; we still inject the template content into the same
// area (draft_body = user_body for send/draft-create).
if userBody != "" {
draftBody = userBody
}

// Plain-text mode: simple append.
if tpl.IsPlainTextMode {
switch kind {
case templateShortcutSend, templateShortcutDraftCreate:
if strings.TrimSpace(draftBody) == "" {
return tplContent
}
return draftBody + "\n\n" + tplContent
default:
// reply/forward plain-text: prepend template before quote.
// emlbuilder composes quote separately so the draft body here
// is pure user-authored content; we just prepend.
if strings.TrimSpace(draftBody) == "" {
return tplContent
}
return tplContent + "\n\n" + draftBody
}
}

switch kind {
case templateShortcutSend, templateShortcutDraftCreate:
if strings.TrimSpace(draftBody) == "" {
return tplContent
}
return draftBody + tplContent
case templateShortcutReply, templateShortcutReplyAll, templateShortcutForward:
// At this compose layer, draftBody is the user-authored area only
// (the caller adds the quote block downstream). Inject template
// content at the head of that area so it lands above the future
// quote block.
if strings.TrimSpace(draftBody) == "" {
return tplContent
}
// Regex replace: if the draft body already contains a quote block
// (some callers pre-compose it), insert template before it.
if draftpkg.HTMLContainsLargeAttachment(draftBody) {
// fall through — no quote heuristic; appending is safe.
}
merged := draftpkg.InsertBeforeQuoteOrAppend(draftBody, tplContent)
if merged != draftBody {
return merged
}
return tplContent + draftBody
}
return draftBody
}
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 | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Lark/Feishu open mail API: do templates with inline cid images automatically render when applied to a draft, or must the draft carry inline MIME parts referencing the file_keys?

💡 Result:

Templates with inline cid images do not automatically render when applied to a draft in Lark/Feishu open mail API. The draft must carry inline MIME parts referencing the file_keys (via cid) for images to render properly. Official API supports constructing emails with inline attachments explicitly via the attachments array (with is_inline=true and cid), or raw EML with proper MIME structure. Lark CLI demonstrates that inline images require either auto-resolution from local paths in body HTML or explicit --inline specification with file_paths and cids; templates likely insert HTML with cid references but do not auto-embed MIME parts unless the API explicitly handles it, which evidence suggests it does not—drafts need MIME parts for rendering.

Citations:


🏁 Script executed:

# Check the template_compose.go file around the function in question
cat -n shortcuts/mail/template_compose.go | head -80 | tail -50

Repository: larksuite/cli

Length of output: 2127


🏁 Script executed:

# Search for AddFileInline usage and where it's called from template-related code
rg -n "AddFileInline" --type go | head -20

Repository: larksuite/cli

Length of output: 1620


🏁 Script executed:

# Search for template-related code that might handle inline images
rg -n "TemplateContent|template.*[Ii]nline" --type go -B 2 -A 2 | head -50

Repository: larksuite/cli

Length of output: 3923


🏁 Script executed:

# Check how mail_send, mail_reply, mail_forward use mergeTemplateBody result
rg -n "mergeTemplateBody" --type go -B 3 -A 10

Repository: larksuite/cli

Length of output: 3024


🏁 Script executed:

# Look for test cases related to templates with inline images
fd -e go "*test*" | xargs rg -l "template.*[Ii]nline|Template.*[Ii]nline" | head -10

Repository: larksuite/cli

Length of output: 459


🏁 Script executed:

# Search for X-Lms-Large-Attachment-Ids or template attachment handling
rg -n "X-Lms-Large-Attachment-Ids|TemplateAttachments|template.*attach" --type go -B 2 -A 2 | head -40

Repository: larksuite/cli

Length of output: 3131


🏁 Script executed:

# Get context around AddFileInline calls in mail_send.go
sed -n '175,205p' shortcuts/mail/mail_send.go

Repository: larksuite/cli

Length of output: 1154


🏁 Script executed:

# Look at the full applyTemplate function and what it returns
rg -n "func applyTemplate|type.*applyTemplate" --type go -A 30 | head -80

Repository: larksuite/cli

Length of output: 4147


🏁 Script executed:

# Check if template attachments are added to the builder anywhere
rg -n "\.Attachments|template.*[Aa]ttach" shortcuts/mail/mail_send.go -B 3 -A 3 | head -60

Repository: larksuite/cli

Length of output: 2976


🏁 Script executed:

# Get the full context of how applyTemplate result is used in mail_send
sed -n '140,250p' shortcuts/mail/mail_send.go

Repository: larksuite/cli

Length of output: 3816


🏁 Script executed:

# Find the templateApplyResult struct definition
rg -n "type templateApplyResult" --type go -A 15

Repository: larksuite/cli

Length of output: 1282


🏁 Script executed:

# Check what happens to template attachments in the compose flow
rg -n "tpl\.Attachments" --type go -B 2 -A 5

Repository: larksuite/cli

Length of output: 9319


🏁 Script executed:

# Check if template inline attachments are ever added to the builder
rg -n "templateAttachment|template.*[Ii]nline.*part|AddFileInline.*template" --type go -B 3 -A 3 | head -80

Repository: larksuite/cli

Length of output: 6132


🏁 Script executed:

# Get the entire applyTemplate function to see what it does with attachments
sed -n '504,600p' shortcuts/mail/template_compose.go

Repository: larksuite/cli

Length of output: 3002


🏁 Script executed:

# Look for tests related to template inline images
fd -type f -name "*test.go" | xargs rg -l "template.*inline|Template.*[Ii]nline" 2>/dev/null

Repository: larksuite/cli

Length of output: 344


🏁 Script executed:

# Check for any documentation or comments about the template + inline mechanism
rg -n "inline.*template|template.*inline|cid.*file_key|file_key.*cid" --type go -B 3 -A 3 | head -100

Repository: larksuite/cli

Length of output: 6098


🏁 Script executed:

# Check the large attachment header mechanism and file_key reuse
rg -n "reuse.*file_key|file_key.*reuse|SMALL.*reused" --type go -B 2 -A 2

Repository: larksuite/cli

Length of output: 1963


🏁 Script executed:

# Look for any test files specifically for template compose
fd -type f -name "*template*test.go"

Repository: larksuite/cli

Length of output: 227


🏁 Script executed:

# Check buildTemplatePayloadFromFlags to understand how inline cids are set
sed -n '350,450p' shortcuts/mail/template_compose.go

Repository: larksuite/cli

Length of output: 3624


🏁 Script executed:

# Check the SKILL.md documentation for templates
cat shortcuts/mail/skills.go 2>/dev/null || grep -r "template.*inline\|inline.*template" skills/ 2>/dev/null | head -20

Repository: larksuite/cli

Length of output: 354


🏁 Script executed:

# Look for any tests related to template compose or inline images
find . -name "*.go" -path "*/test*" -o -name "*_test.go" | xargs grep -l "template" 2>/dev/null | head -5

Repository: larksuite/cli

Length of output: 206


🏁 Script executed:

# Check if there are any TODOs or FIXMEs related to template inline images
rg -n "TODO|FIXME|BUG|XXX" shortcuts/mail/template_compose.go | grep -i "inline\|cid\|attach"

Repository: larksuite/cli

Length of output: 39


🏁 Script executed:

# Verify if the server actually supports materializing inline images from template file_keys
rg -n "server.*reuse\|reuse.*server\|materialize" shortcuts/mail/ -B 2 -A 2

Repository: larksuite/cli

Length of output: 39


🏁 Script executed:

# Check the mail API documentation files to understand template behavior
find . -name "*.md" -path "*/lark-mail/*" | xargs grep -l "template" 2>/dev/null

Repository: larksuite/cli

Length of output: 244


🏁 Script executed:

# Read the templates documentation to understand the expected behavior
cat skills/lark-mail/references/lark-mail-templates.md | head -200

Repository: larksuite/cli

Length of output: 7210


🏁 Script executed:

# Check specifically for any documentation about inline image rendering when templates are applied
cat skills/lark-mail/references/lark-mail-templates.md | grep -A 10 -B 5 "inline\|cid\|image"

Repository: larksuite/cli

Length of output: 3313


🏁 Script executed:

# Check how the draft is created and whether template attachment metadata is sent
sed -n '200,230p' shortcuts/mail/mail_send.go

Repository: larksuite/cli

Length of output: 1300


🏁 Script executed:

# Look at what data is sent to drafts.create or drafts.send
rg -n "CreateWithRaw\|draftpkg.Send\|drafts.create\|drafts.send" shortcuts/mail/ -B 5 -A 5 | head -80

Repository: larksuite/cli

Length of output: 39


🏁 Script executed:

# Check the draft package to see what metadata might be sent
find . -name "*.go" -path "*/draft/*" | xargs grep -l "attachment\|Attachment" | head -3

Repository: larksuite/cli

Length of output: 201


🏁 Script executed:

# Verify whether template SMALL attachments are included in any request
rg -n "SMALL\|Small" shortcuts/mail/template_compose.go -B 3 -A 3

Repository: larksuite/cli

Length of output: 39


🏁 Script executed:

# Check if there's any mechanism to pass template attachment metadata to the server in the draft payload
rg -n "CreateWithRaw\|Send" shortcuts/mail/draft/ -l

Repository: larksuite/cli

Length of output: 39


🏁 Script executed:

# Check applyTemplate result and how it's used - specifically for all attachments, not just LARGE
sed -n '555,595p' shortcuts/mail/template_compose.go

Repository: larksuite/cli

Length of output: 1529


🏁 Script executed:

# Check if there's any code that processes template attachments separately for inline vs non-inline
rg -n "\.IsInline\|isInline" shortcuts/mail/ --type go -B 2 -A 2 | head -60

Repository: larksuite/cli

Length of output: 39


🏁 Script executed:

# Check the full context of mergeTemplateBody and what it receives
sed -n '470,490p' shortcuts/mail/template_compose.go

Repository: larksuite/cli

Length of output: 882


🏁 Script executed:

# Verify: do any of the mail_send/reply/forward callers iterate over tpl.Attachments to add them?
rg -n "for.*tpl\.Attachments\|for.*Attachments.*range" shortcuts/mail/ --type go

Repository: larksuite/cli

Length of output: 39


Template inline images (cid:) will render as broken images in sent/draft messages

mergeTemplateBody embeds tpl.TemplateContent (containing <img src="cid:..."> references) directly into the draft, but applyTemplate only returns LargeAttachmentIDs—it discards the IsInline and CID metadata from template attachments. Neither LARGE nor SMALL template inline attachments are added to the EML builder: LARGE attachments are propagated via the X-Lms-Large-Attachment-Ids header, while SMALL attachments rely on undocumented "server-side reuse by file_key" semantics, which is insufficient for inline images. The Lark/Feishu API does not auto-materialize cid: → file_key bindings; the draft must carry inline MIME parts via AddFileInline to render correctly.

To fix, templateApplyResult must carry both large IDs and inline attachment metadata (path, CID), and callers must call AddFileInline for each inline template attachment before sending/drafting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/template_compose.go` around lines 604 - 658, mergeTemplateBody
correctly injects tpl.TemplateContent (with <img src="cid:...">) but
applyTemplate and templateApplyResult only propagate LargeAttachmentIDs and drop
inline attachment metadata, so inline images never get added via AddFileInline
and render broken; to fix, extend templateApplyResult to include inline
attachment info (e.g., struct fields for IsInline, CID, and file path/file_key),
update applyTemplate to populate those inline entries for both LARGE and SMALL
template attachments, and then update the downstream send/draft code paths
(where EML is built and headers like X-Lms-Large-Attachment-Ids are set) to
iterate over templateApplyResult.inlineAttachments and call AddFileInline for
each before constructing/sending the EML so cid: references are satisfied.

3. 原 HTML 改写为 `<img src="cid:<uuid>">`
4. 在 `attachments[]` 追加 `{id: <file_key>, cid, is_inline: true, filename, attachment_type}`

**LARGE 切换**:本地单文件 ≤ 20 MB 走单次上传、> 20 MB 走分块上传;`attachment_type` 则看累计 EML 投影(subject / to / cc / bcc / template_content + base64 附件体积),到 25 MB 后同批次剩余附件标 `LARGE`。两套判定相互独立。
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

LARGE 阈值与代码不一致:应为 20 MB,不是 25 MB。

templateAttachmentBuilder.append 中判定逻辑为 if b.projectedSize > 20*1024*1024 { b.largeBucket = true }(见 shortcuts/mail/template_compose.go:326-328)。本行和下文 Q4(Line 108)都写成 25 MB,会让用户对切换时机判断有误。

✏️ Proposed fix
-**LARGE 切换**:本地单文件 ≤ 20 MB 走单次上传、> 20 MB 走分块上传;`attachment_type` 则看累计 EML 投影(subject / to / cc / bcc / template_content + base64 附件体积),到 25 MB 后同批次剩余附件标 `LARGE`。两套判定相互独立。
+**LARGE 切换**:本地单文件 ≤ 20 MB 走单次上传、> 20 MB 走分块上传;`attachment_type` 则看累计 EML 投影(subject / to / cc / bcc / template_content + base64 附件体积),累计超过 20 MB 后同批次剩余附件标 `LARGE`。两套判定相互独立。

And in Line 108:

-| Q4 附件 | 所有 5 个 Shortcut | 过期 / 封禁超大附件丢弃;`emlProjectedSize` 累计 > 25MB 则模板小附件改标 LARGE;去重键 = `Attachment.id`(Drive file_key);顺序 = 草稿在前、模板在后 |
+| Q4 附件 | 所有 5 个 Shortcut | 过期 / 封禁超大附件丢弃;`emlProjectedSize` 累计 > 20MB 则模板小附件改标 LARGE;去重键 = `Attachment.id`(Drive file_key);顺序 = 草稿在前、模板在后 |
📝 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
**LARGE 切换**:本地单文件 ≤ 20 MB 走单次上传、> 20 MB 走分块上传;`attachment_type` 则看累计 EML 投影(subject / to / cc / bcc / template_content + base64 附件体积),到 25 MB 后同批次剩余附件标 `LARGE`。两套判定相互独立。
**LARGE 切换**:本地单文件 ≤ 20 MB 走单次上传、> 20 MB 走分块上传;`attachment_type` 则看累计 EML 投影(subject / to / cc / bcc / template_content + base64 附件体积),累计超过 20 MB 后同批次剩余附件标 `LARGE`。两套判定相互独立。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/lark-mail/references/lark-mail-templates.md` at line 39, The doc
states the LARGE threshold as 25 MB but the implementation in
templateAttachmentBuilder.append uses 20*1024*1024; update the markdown to match
the code by changing both occurrences of "25 MB" to "20 MB" (so the description
of switching and the Q4 line read 20 MB) to reflect the logic in
templateAttachmentBuilder.append in template_compose.go where b.projectedSize >
20*1024*1024 sets b.largeBucket = true.

Comment on lines +115 to +117
```
warning: template to/cc/bcc are appended without de-duplication; you may see repeated recipients. Use --to/--cc/--bcc to override, or run +template-update to clear template addresses.
```
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

Add a language tag to the fenced block.

markdownlint MD040 flags this; use text (or an appropriate language) so renderers pick sensible defaults.

✏️ Proposed fix
-```
+```text
 warning: template to/cc/bcc are appended without de-duplication; you may see repeated recipients. Use --to/--cc/--bcc to override, or run +template-update to clear template addresses.
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 115-115: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/lark-mail/references/lark-mail-templates.md` around lines 115 - 117,
The fenced code block containing the warning string "warning: template to/cc/bcc
are appended without de-duplication; you may see repeated recipients. Use
--to/--cc/--bcc to override, or run +template-update to clear template
addresses." needs a language tag to satisfy markdownlint MD040; update the
triple-backtick fence that surrounds that warning to use a language like text
(e.g., ```text) so renderers and linters pick sensible defaults.

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.

1 participant