Add example commands for file types declared in the supported-conversions
table but absent from the command examples section: .docx/.doc, .txt,
.html, .xls -> sheet, and .csv -> sheet.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(strict-mode): reject explicit --as instead of silently overriding it
ResolveAs checked strict mode before the --as flag, so `--as bot` under strict=user
was silently rewritten to user. Reorder so explicit --as is returned as-is and CheckStrictMode rejects the conflict (exit=2). Implicit paths (--as auto / unset) are still forced by
strict mode.
* fix(strict-mode): fix CI
Project management for Lark/Feishu is provided by the standalone
meegle-cli (https://github.com/larksuite/meegle-cli), which requires a
separate install. Surface it in the Features table so users can
discover the capability without expecting it to ship inside lark-cli.
Add --at-chatter-ids flag to shortcuts/im/im_messages_search.go that
passes filter.at_chatter_ids to the search API, restricting results to
messages that @mention any of the given user open_ids. Messages that
* feat(pagination): preserve pagination state on truncation and natural end
* feat(pagination): drop page_token from merged output to reflect aggregate view
Expose doc_wiki/search v2 under the drive domain via explicit flags
(--query, --edited-since, --commented-since, --opened-since,
--created-since, --mine, --creator-ids, --doc-types, --folder-tokens,
--space-ids, ...) instead of a nested JSON filter, so natural-language
queries from AI agents map 1:1 to discrete flags.
Time handling:
- my_edit_time and my_comment_time are snapped to the hour (floor/ceil)
with a stderr notice, since those fields are aggregated at hour
granularity server-side. create_time passes through as-is.
- open_time has a server-side 3-month cap per request. When
--opened-since / --opened-until span exceeds 90 days, the CLI narrows
the request to the most recent 90-day slice and emits a stderr notice
listing every remaining slice's --opened-* values so the agent can
re-invoke for older ranges. Spans over 365 days are rejected up front
to bound runaway slicing.
Flag ergonomics:
- --doc-types accepts mixed case; values are normalized to upper case
before validation and before being sent to the server.
- --sort default is translated to the server enum DEFAULT_TYPE (every
other sort value upper-cases 1:1).
Error hints:
- Lark code 99992351 (referenced open_id outside the app's contact
visibility) is enriched with a +search-specific hint that
distinguishes API scope from contact visibility and points at
--creator-ids / --sharer-ids as the likely source.
Skill docs:
- new reference at skills/lark-drive/references/lark-drive-search.md,
including the open_time slicing protocol and the paginate-within-
slice-before-switching agent playbook.
- lark-drive/SKILL.md routes resource-discovery to drive +search.
- lark-doc/SKILL.md and lark-doc-search.md mark docs +search as
deprecated and point users at drive +search.
Change-Id: I36d620045809b448446d4fdbdfa923b05794da19
* feat(mail): add +share-to-chat shortcut to share emails as IM cards
Two-step API (create share token → send card) wrapped in a single
shortcut. Supports message-id/thread-id, five receive-id-type variants
(chat_id, open_id, user_id, union_id, email), and dry-run mode.
Change-Id: Ic7b8c01c0d25fef262f35be92555f1fd019bd679
Co-Authored-By: AI
* fix(mail): regenerate SKILL.md from skill-template instead of manual edit
Add missing safety rule 8 (draft link rule) to skill-template/domains/mail.md
so it survives regeneration. SKILL.md is now produced by `make gen-skills`
in the registry repo rather than hand-edited.
Change-Id: I9cf3605deae8b6de2042e40819fedc304967e78e
Co-Authored-By: AI
* fix(mail): add docstrings and use real validation path in tests
- Add Go doc comments to exported symbols for docstring coverage
- Rewrite tests to exercise MailShareToChat.Validate via RuntimeContext
instead of duplicating validation logic
- Replace hand-rolled containsStr with strings.Contains
- Add httpmock stubs for execute and error path tests
Change-Id: Ic781494f61e9e844224185844bce7b0c48e8e200
Co-Authored-By: AI
* test(mail): add dry-run E2E test for +share-to-chat
Validate request shape (method, URL, mailbox path) under --dry-run
with fake credentials. Covers message-id, thread-id, and custom
mailbox variants.
Change-Id: Iae87bf141cbe4f312d3e9b1fca4ba175052c5c35
Co-Authored-By: AI
* fix(mail): include request body and params in dry-run output
DryRun now mirrors Execute: the share-token POST shows message_id or
thread_id, and the send POST shows receive_id_type and receive_id.
E2E test updated to assert these fields. Also fix strconv.Itoa usage.
Change-Id: I00f8770fd5a12b7354986c5e5077f97cfe5d6653
* style(mail): gofmt dry-run test file
Change-Id: I47dc6a9a47252dcfb7853737f88dfdaef65a0ae7
* test(mail): assert exact API call count in dry-run test
Change-Id: I9f4a1a183b55d03f5248eb4adddfddb08037ca95
End-to-end RFC 3798 Message Disposition Notification support, covering
both sides of the receipt flow — requesting a receipt when composing, and
responding to one (send or decline) when reading.
Request side (compose)
- New --request-receipt flag on +send / +reply / +reply-all / +forward /
+draft-create / +draft-edit. When set, the outgoing EML carries a
Disposition-Notification-To header (RFC 3798) addressed to the resolved
sender. Recipient mail clients may prompt the user, auto-send a receipt,
or silently ignore — delivery is not guaranteed.
- requireSenderForRequestReceipt gates the flag against a controlled
sender address resolved BEFORE the orig.headTo fallback in +reply /
+reply-all / +forward, so the DNT cannot silently land on someone else
in CC / shared-mailbox flows.
Response side
- +send-receipt: build a system-templated reply for messages carrying the
READ_RECEIPT_REQUEST label (-607). Subject / recipient / sent / read
time layout matches the Lark client; body is non-customizable — receipt
bodies are system templates by industry convention; free-form notes
belong in +reply. Risk:"high-risk-write" + --yes required.
- +decline-receipt: clear READ_RECEIPT_REQUEST without sending anything
(mirrors the client's "不发送" / "Don't send" button). Idempotent on
re-run; Risk:"write" — no --yes needed.
Read-path hints
- +message / +messages / +thread emit a stderr hint when surfacing a
mail carrying READ_RECEIPT_REQUEST, exposing BOTH response paths
(+send-receipt --yes / +decline-receipt) so agents present a real
choice to the user instead of silently auto-sending.
Guard rails
- +send / +reply / +reply-all / +forward stay draft-by-default and
require --confirm-send to send, gated by a dynamic scope check for
mail:user_mailbox.message:send (absent from the default scope set so
draft-only flows don't need the sensitive permission).
- All header-bound user input (sender / display name / recipient /
subject) goes through CR/LF rejection plus Bidi / zero-width / line-
separator guards, mirroring emlbuilder.validateHeaderValue, to block
header injection and visual spoofing.
- Hint output strips terminal control characters (CR, LF) from any
untrusted field embedded into the user-visible suggestion.
Backend coupling
- Outgoing receipt EML carries the private header
X-Lark-Read-Receipt-Mail: 1. The data-access backend parses it into
BodyExtra.IsReadReceiptMail; DraftSend then applies READ_RECEIPT_SENT
(-608) and clears READ_RECEIPT_REQUEST (-607) from the original
message, closing the client-side banner.
- en receipts require backend TCC SubjectPrefixListForAdvancedSearch to
include "Read Receipt:" for conversation-view aggregation; zh prefix
("已读回执:") is already configured.
Docs: new reference pages for +send-receipt / +decline-receipt;
--request-receipt noted on each compose-side reference; SKILL.md
workflow (section 9) describes the full privacy-safe decision tree on
both sides.
Tests cover emlbuilder DispositionNotificationTo / IsReadReceiptMail
helpers, receiptMetaLabels (zh / en), buildReceiptSubject, text and HTML
body generators (with HTML escaping and Bidi guards), header-injection
defenses, sender-resolution gating (CC-only / shared-mailbox regression),
hint emission paths, and the full +send-receipt / +decline-receipt happy
+ idempotent paths via httpmock.
Update im +chat-messages-list to request only thread root messages from /open-apis/im/v1/messages by default. This aligns the shortcut request shape with topic-group usage and makes the intended API behavior explicit in both runtime params and dry-run output.
Change-Id: I3901b27e70b0e4db506ff199eb03c96fcf98671d
Adds an `--api-version v2` path to the docs shortcuts, backed by the
`docs_ai/v1/documents` OpenAPI. DocxXML is the default document format
and Markdown is available as an alternative. Content input is unified
across the three shortcuts via `--content` + `--doc-format`. The v1
(MCP) path is preserved for backward compatibility and now prints a
deprecation notice on use.
Shortcuts:
- `docs +create --api-version v2`: create a document from XML or
Markdown, with optional `--parent-token` or `--parent-position`.
Bot identity continues to auto-grant the current CLI user
full_access on the new document.
- `docs +fetch --api-version v2`: adds `--detail simple|with-ids|full`
for export granularity and `--scope full|outline|range|keyword|section`
for partial reads, along with `--context-before` / `--context-after`,
`--max-depth`, and `--revision-id`.
- `docs +update --api-version v2`: introduces structured operations
via `--command`: `str_replace`, `block_delete`, `block_insert_after`,
`block_copy_insert_after`, `block_replace`, `block_move_after`,
`overwrite`, `append`.
Framework support in `shortcuts/common`:
- `OutRaw` / `OutFormatRaw` emit the JSON envelope with HTML escaping
disabled so XML/HTML document bodies are preserved verbatim.
- New `Shortcut.PostMount` hook runs after a cobra.Command is fully
configured; used here to install a version-aware help function
that hides flags belonging to the inactive `--api-version`.
Also refreshes the lark-doc skill pack (SKILL.md, create/fetch/update
references, new lark-doc-xml and lark-doc-md references, style and
workflow guides), README examples, and downstream skill call sites
(lark-drive, lark-vc, lark-whiteboard, lark-workflow-meeting-summary,
lark-event).
Change-Id: Ide2d86b190a4e21095ae29096e7fb00031d80489
Give each AI Agent (OpenClaw, Hermes) its own lark-cli workspace so
its Feishu calls don't overwrite the developer's local config or
collide with other Agents.
lark-cli config bind [--source openclaw|hermes] [--app-id <id>]
[--identity bot-only|user-default] [--force]
Key capabilities:
- Source auto-detected from OPENCLAW_* / HERMES_* env signals; config
written to ~/.lark-cli/<agent>/, isolated per Agent.
- Two identity presets: 'bot-only' (flag-mode default) and
'user-default'. Flag mode rejects silent bot→user escalation
without --force; TUI prompts are exempt.
- Agent-friendly stdout JSON with 'identity' + 'message' for
next-step branching.
- 'config show' and 'doctor' expose the bound 'workspace'.
- OpenClaw SecretRef resolution: plain / ${VAR} / file:+JSON Pointer
/ exec:.
* refactor: make install.js side-effect-free on require
Change-Id: I5444e3f34642d7c0740b6422a70ca6921a85e363
* feat: add getExpectedChecksum with unit tests
Change-Id: I87548be25d30c384e743da17b1d161b9d9f0ea87
* feat: add verifyChecksum with unit tests
Change-Id: Ifc2067bf1b824b02257dba7b53716fbe18d0f6b6
* feat: harden download with host allowlist and checksum verification
Change-Id: I2580782866049f1f62a2597e86b7bf59d0e50925
* ci: bundle checksums.txt in npm package for install verification
Change-Id: I2d7c44d9d5b9075158f63c0f8cf66c1e0abe3d8d
* ci: use triggering tag and verify checksums.txt presence in release workflow
Address CodeRabbit review: use GITHUB_REF_NAME instead of parsing
package.json to avoid version drift, and add explicit file check to
fail loudly if checksums.txt is missing or empty.
Change-Id: I8a5658412b6afc338ad2a642baba146cceafd0fc
* feat: streaming hash, allowlist tests, and malformed-line coverage
- verifyChecksum: switch from readFileSync to streaming 64KB chunks
to avoid loading entire archive (10-100MB) into memory
- Export and test assertAllowedHost: 7 cases covering allowed hosts,
rejection, case normalization, port handling, invalid URL
- Add ALLOWED_HOSTS comment clarifying it only gates initial URL
- Add getExpectedChecksum tests for malformed/tab-separated lines
Change-Id: Ida639def89c242b3b261a76effae08fd414a10dc
* refactor(base): enforce field-map record upsert input
1. Reject top-level fields wrappers in base +record-upsert input and keep request bodies as field maps.
2. Replace record-upsert tests with Map<FieldNameOrID, CellValue> input and assert the outgoing body has no fields wrapper.
3. Consolidate Base record value documentation around lark-base-cell-value and update record command references.
* refactor(base): use common record JSON parsing for upsert
1. Remove the dedicated record-upsert parser and restore the shared record JSON object validation path.
2. Keep record-upsert dry-run and execution as raw JSON object passthrough.
3. Drop the test assertion that rejected a top-level fields key for record-upsert.
* docs(base): refine record cell value guidance
1. Align record CellValue examples with live behavior for date, URL, user, link, select, numeric styles, and readonly fields.
2. Remove misleading user_id_type and execution identity prompts from record-writing guidance.
3. Keep record JSON file input guidance generic and avoid documenting environment-specific stdin or path limits.
Introduces `lark-cli slides +replace-slide`, a shortcut over the
native `xml_presentation.slide.replace` API for element-level editing
of existing Lark Slides pages. Callers pass a JSON array of parts and
the CLI handles URL resolution, XML hygiene, client-side validation,
and 3350001 hint enrichment.
Why a dedicated shortcut
The native API has three sharp edges every caller hits:
1. URL formats. Users have /slides/<token> or /wiki/<token> URLs, not
bare xml_presentation_id.
2. Undocumented XML hygiene. `block_replace` requires id=<block_id> on
the replacement root; <shape> requires <content/>. Missing either
returns a catch-all 3350001 with no guidance.
3. 3350001 is a catch-all on the backend with no actionable message.
Code
shortcuts/slides/slides_replace_slide.go (new)
- Flags: --presentation (bare token | /slides/ URL | /wiki/ URL),
--slide-id, --parts (JSON array, max 200), --revision-id (-1 for
current, specific number for optimistic locking), --tid,
--as user|bot.
- Validation (pre-API): [1,200] item cap; action restricted to
block_replace / block_insert (str_replace rejected); per-action
required fields (block_id for block_replace, insertion for
block_insert); per-field string type-assertion guards on the
decoded JSON so a numeric/bool payload fails fast with a targeted
error.
- XML hygiene:
* injects id="<block_id>" on block_replace replacement roots;
* auto-expands self-closing <shape/> and injects <content/> on
shapes for SML 2.0 compliance.
Dry-run surfaces injection errors and renders the same
path-encoded presentationID that Execute sends.
- On backend 3350001 attaches a generic common-causes checklist
(missing block_id / invalid XML / coords out of 960×540).
shortcuts/slides/helpers.go
- ensureXMLRootID: regex tightened to `(?:^|\s)id` so data-id and
xml:id are not matched as root id.
- ensureShapeHasContent: regex `<content(?:\s|/|>)` avoids false
positives like <contention/>; self-closing branch preserves
trailing siblings.
shortcuts/slides/shortcuts.go: register SlidesReplaceSlide.
Tests (package coverage 89.4%; parseReplaceParts and
injectBlockReplaceIDs both reach 100%)
- helpers_test.go: regex edge cases, id override semantics, content
auto-inject across self-closing and open-tag shapes.
- slides_replace_slide_test.go: parameter validation table, URL
resolution (slides / wiki), mixed block_replace + block_insert,
size boundaries, auto-inject behavior, 3350001 hint enrichment,
per-field type-assertion guards, whitespace-only --parts guard
(distinct from the `[]` "at least 1 item" path), replacement
without root element surfaces pre-flight instead of reaching the
backend, and a tight negative assertion that non-3350001 errors
get no slides-specific hint.
Docs (skills/lark-slides)
- SKILL.md: add +replace-slide to the Shortcuts table, register the
new xml_presentation.slide.get / .replace native endpoints,
update core rule 7 to prefer block-level replace over full-page
rebuild now that element-level editing exists, extend the error
table with 3350001 / 3350002 pointing at the replace-slide doc,
add "add image to existing slide via block_insert" as an explicit
Workflow step and symptom-table entry, and refresh the reference
index to include the three new docs below. The old "整页替换" 4-rule
checklist is retired — its one still-relevant guard (new <img>
avoiding overlap) is preserved in the symptom table.
- New references:
* lark-slides-replace-slide.md — flags, parts schema, auto-inject
notes, mixed-action support, 200-item cap, revision_id
semantics, error table, and a "合法根元素速查" cheatsheet for
the eight supported root elements (shape / line / polyline /
img / icon / table / td / chart) with minimal verified XML
snippets. Explicit unsupported list: video / audio / whiteboard
(these appear only as <undefined> export placeholders in SML 2.0).
* lark-slides-edit-workflows.md — recipe-style edit flows covering
the read → modify → write loop and the block_replace vs
block_insert decision tree.
* lark-slides-xml-presentation-slide-get.md — native read API with
block_id extraction examples.
- Fixes across existing references:
* replace / create / delete / presentations.get: add the .data
wrapper in return-value examples, correct jq paths.
* media-upload: fix jq path .file_token → .data.file_token.
* examples.md: annotate auto-inject behavior, replace the
incorrect failed_part_index example with the actual 3350001
error shape.
Empirical corrections (BOE-verified)
- revision_id: stale-but-existing values are accepted; only values
greater than current return 3350002.
- Wrong block_id returns 3350001, not a 200 with failed_part_index.
- Mixed block_replace + block_insert in one call is supported.
- Type-mismatched block_replace (e.g. shape id with a <td>
replacement) is silently accepted by the backend and may destroy
content; 3350001 specifically signals a missing block_id.
Unify lark-cli im +messages-search pagination flags to use int semantics consistently.
Previously, page-limit was registered as an int flag while page-size was still handled as a string flag and parsed manually. This led to inconsistent runtime behavior inside the same shortcut and allowed test helpers to drift from the real CLI flag registration.
Change-Id: Ic4876f4ca7f410a8fe3234e08e41b54ce26990d9
* feat: unify minute artifacts output to ./minutes/{minute_token}/
* fix: tighten path validation and batch-mode --output rejection
* style: translate comments to english and trim historical context
* style: translate leftover chinese comments in vc_notes
* refactor: address review findings across validate ordering, error types, JSON, tests
* fix: sanitize server-provided filename to prevent escape from artifact dir
* style: tighten flag help text for minutes/vc output flags
* docs: update minutes/vc skill docs for unified artifact layout
Add lark-cli wiki +delete-space to delete a knowledge space via
DELETE /open-apis/wiki/v2/spaces/:space_id. When the API returns an
async task_id, the shortcut polls /open-apis/wiki/v2/tasks/:task_id
with task_type=delete_space for a bounded window and emits a
next_command pointing to drive +task_result on timeout. A new
wiki_delete_space scenario is added to drive +task_result for resuming
timed-out deletes.
Change-Id: I75da52b617c206fb778a493ffaa200adf7920a27
* feat(doc): add --from-clipboard flag to docs +media-insert
Allow users to upload the current clipboard image directly to a Lark
document without saving to a local file first.
- New --from-clipboard bool flag (mutually exclusive with --file)
- shortcuts/doc/clipboard.go: readClipboardToTempFile() with per-OS impl
macOS — osascript (built-in, no extra deps)
Windows — PowerShell + System.Windows.Forms (built-in)
Linux — tries xclip / wl-paste / xsel in order; clear install hint
on failure
- No new Go dependencies, no Cgo
- Temp file is created before upload and removed via defer cleanup()
- --file changed from Required:true to optional; Validate enforces
exactly-one of --file / --from-clipboard
* fix(doc): fix clipboard image read on macOS for screenshots and browser-copied images
- Add TIFF fallback (macOS screenshots default to TIFF, not PNG)
- Add HTML base64 fallback (images copied from Feishu/browser embed data URI)
- Use current directory for temp file so FileIO path validation passes
* fix(doc): scan HTML/RTF/text clipboard formats for base64 image data URIs
Extend attempt-3 fallback to iterate all text-based clipboard formats
(HTML, RTF, UTF-8, plain text) rather than only HTML. Any format that
contains a "data:<mime>;base64,<data>" pattern is accepted, covering
images copied from Feishu, Chrome, Safari, and other apps that embed
base64 in non-HTML clipboard slots. Also handle URL-safe base64.
* test(doc): add unit tests for clipboard helpers to meet 60% coverage threshold
Cover decodeHex, hexVal, decodeOsascriptData, reBase64DataURI, and
extractBase64ImageFromClipboard (via fake osascript on PATH).
Package coverage: 57% → 61.2%.
* fix(doc): address CodeRabbit review comments on clipboard feature
- Extend reBase64DataURI regex to cover URL-safe base64 chars (-_) so
URL-safe payloads are matched before decoding is attempted
- Fix readClipboardLinux to continue to next tool when a found tool
returns empty output instead of failing immediately
- Guard fake-osascript test with runtime.GOOS == "darwin" skip
- Use os.PathListSeparator instead of hardcoded ":" in test PATH setup
* fix(doc): replace os.* temp-file clipboard path with in-memory streaming
Fixes forbidigo lint violations in shortcuts/doc: os.CreateTemp, os.Remove,
os.Stat, os.WriteFile are banned in shortcuts/; replaced with vfs.* equivalents
for sips TIFF→PNG conversion, and eliminated temp files entirely elsewhere by
having platform clipboard readers return []byte directly.
- readClipboardDarwin: osascript outputs hex literals decoded in Go (no file I/O)
- readClipboardWindows: PowerShell outputs base64 to stdout, decoded in Go
- readClipboardLinux: tool stdout bytes returned directly
- convertTIFFToPNGViaSips: still needs temp files — uses vfs.CreateTemp/Remove
- DriveMediaUploadAllConfig/DriveMediaMultipartUploadConfig: add Content io.Reader
field so in-memory clipboard bytes skip FileIO.Open() path
- Fix ineffassign in clipboard_test.go (scriptBody double-assignment)
- Update TestReadClipboardLinux_NoToolsReturnsError for new signature
* fix(doc): address CodeRabbit review comments on Linux clipboard path
- Update --from-clipboard flag description to list xclip, xsel and wl-paste
- Preserve last backend-specific error in readClipboardLinux so users see
a meaningful message when a tool is found but fails
- Validate PNG magic bytes for xsel output (xsel cannot negotiate MIME types)
- Add URL-safe base64 regression test for reBase64DataURI
* fix(doc): strip whitespace from base64 payload before decoding clipboard data URI
HTML and RTF clipboard content often line-wraps base64 at 76 characters.
FindSubmatch returns the raw wrapped token so direct decode would fail.
Normalize whitespace with strings.Fields before passing to base64.Decode.
* fix(doc): drop TIFF fallback and internal/vfs import on macOS clipboard
depguard rule shortcuts-no-vfs forbids shortcuts/ from importing
internal/vfs directly. The only caller was the sips TIFF→PNG
conversion, which was already a fragile best-effort fallback that
required temp files.
Remove the TIFF fallback entirely; the remaining two attempts cover
the real-world cases:
1. osascript → PNG hex literal — native screenshots and most apps
2. scan text clipboard formats for base64 data URI — Feishu/browsers
* test(doc): cover readClipboardLinux xsel PNG validation and dispatcher path
Added tests:
- TestReadClipboardLinux_XselRejectsNonPNG: fake xsel that returns plain
text is rejected by the PNG-magic check, preventing text from being
uploaded as an "image".
- TestHasPNGMagic: table-driven coverage of the PNG signature check.
- TestReadClipboardImageBytes_UnsupportedPlatform: exercises the shared
dispatcher post-processing and asserts the (nil, nil) invariant.
Raises clipboard.go diff coverage and brings the package from 61.6% to
63.8% overall.
* test: cover in-memory Content upload paths for clipboard feature
Adds unit tests for the new Content io.Reader branches introduced by
the clipboard feature:
- UploadDriveMediaAll with in-memory Content (drive_media_upload.go 87.5%)
- UploadDriveMediaMultipart with in-memory Content (84.6%)
- uploadDocMediaFile single-part and multipart with clipboard bytes
(doc_media_upload.go 0% -> 88.9%)
Adds TestNewRuntimeContextForAPI helper that wires Factory, context,
and bot identity so package tests can invoke DoAPI without mounting
the full cobra command tree.
* test: cover clipboard Validate/DryRun branches and testing helper
Adds unit tests for the clipboard-related Validate/DryRun paths that
Codecov patch-coverage was flagging as uncovered:
- Validate error when neither --file nor --from-clipboard is supplied
- Validate error when both are supplied (mutual exclusion)
- DryRun output contains <clipboard image> placeholder
- Self-test for TestNewRuntimeContextForAPI so shortcuts/common
sees coverage for the new helper (not just shortcuts/doc)
* test: cover Execute clipboard branch via injectable readClipboardImage
Makes readClipboardImageBytes swappable in tests by routing the call
through a package-level variable readClipboardImage. Tests inject a
synthetic PNG payload so the full Execute clipboard flow
(resolve → create block → upload in-memory bytes → bind) runs under
unit test without a real pasteboard.
Covers:
- TestDocMediaInsertExecuteFromClipboard: end-to-end happy path
- TestDocMediaInsertExecuteClipboardReadError: early-return on
readClipboardImage() failure
* ci: re-trigger pull_request workflow for PR #508
Previous push to 9dedb7a did not trigger the main CI workflow via
the pull_request event (only PR Labels ran). The workflow_dispatch
run I triggered manually lacks PR-scoped secrets so security and
e2e-live failed. An empty commit replays the pull_request event so
the full matrix (deadcode, license-header, security, e2e-live) runs
with proper context.
* test(doc): guard info.Size() behind err check to prevent nil-deref
CodeRabbit flagged that 't.Fatalf("... size=%d err=%v", info.Size(), err)'
evaluates info.Size() even when os.Stat returned (nil, err), which nil-derefs.
Split the check into two stages so the error-path t.Fatalf does not touch
info.
* fix(doc): address fangshuyu-768 review on clipboard PR
Seven code changes driven by review feedback:
1. clipboard.go: stop using CombinedOutput() on osascript / powershell.
Stdout is decoded, stderr is captured separately via cmd.Stderr and
surfaced in the terminal error message, so locale warnings or
AppleEvent permission prompts no longer pollute the hex/base64
payload or mask the real failure.
2. clipboard.go: validate decoded base64 data URI bytes against known
image magic headers (PNG/JPEG/GIF/WebP/BMP). A text clipboard that
happens to contain a literal 'data:image/...;base64,...' fragment
(documentation, tutorials, pasted HTML source) no longer silently
becomes an image upload.
3. clipboard.go: simplify the Linux 'no tool found' install hint to a
distro-agnostic phrasing instead of apt/yum only.
4. clipboard_test.go: delete the stale TestReadClipboardToTempFile_*
tests. They referenced a readClipboardToTempFile function that no
longer exists and only exercised os.CreateTemp/os.Remove. Replace
with TestReadClipboardImageBytes_EmptyResultReturnsError which
actually locks in the 'empty clipboard' → error contract of the
current API (Linux-only since mac/Windows need a real pasteboard).
5. doc_media_upload.go: introduce UploadDocMediaFileConfig struct so
uploadDocMediaFile takes a named config instead of 8 positional
params. Drops the //nolint:lll the old call site had to carry.
6. doc_media_insert.go: convert the clipboard upload call to the new
config struct and only set Config.Content when the clipboard branch
actually produced bytes — this also fixes a latent typed-nil bug
where a nil *bytes.Reader was being passed through an io.Reader
parameter, which tripped the 'if cfg.Content != nil' check in
UploadDriveMediaAll and crashed --file uploads.
7. shortcuts/common/testing.go: TestNewRuntimeContextForAPI now takes
the identity as an explicit core.Identity parameter instead of
hardcoding core.AsBot, and its self-test covers both AsBot and
AsUser. Existing call sites pass core.AsBot explicitly.
Also annotates DryRun output with an 'upload_size_note' when
--from-clipboard is set, since DryRun never reads the pasteboard and
can't predict whether the payload will take the single-part or
multipart path.
* fix(doc): capture line-wrapped base64 in clipboard data URI regex (#586)
HTML and RTF clipboard content commonly folds base64 payloads at
76 chars (standard MIME folding). The previous character class
[A-Za-z0-9+/\-_]+=* stopped at the first \n, so the downstream
strings.Fields normalisation was a no-op (nothing to strip) and
extractBase64ImageFromClipboard silently uploaded a truncated
payload whose 8-byte prefix happened to pass hasKnownImageMagic.
Extend the class to include \s so the Fields strip actually has
whitespace to remove before base64 decoding. Terminators (", <,
), ;) remain outside the class so the match still ends at the
URI boundary.
Add TestReBase64DataURI_LineWrapped covering \n, \r\n, and \t
folds, full round-trip byte-equality, and the terminator-boundary
invariant so any future regression trips a failing test.
* docs(skill): add clipboard-empty fallback guidance for +media-insert
When --from-clipboard returns 'no image data' (empty clipboard, non-image
content, or Linux without xclip/wl-paste/xsel), the agent must NOT silently
swallow the error. It should tell the user the clipboard had no image, ask
for a local file path, then retry the same insert command with --file.
Lists three anti-patterns (silent success, guessing a file path, pre-emptive
save-then-file workaround) that agents have been tempted into.
* docs(skill): user-stated source trumps clipboard/file heuristic
The heuristic table (prefer --from-clipboard when image is on the
clipboard) is a fallback for when the user is vague. If the user
explicitly says 'use the screenshot I just copied' → clipboard; if
they give a path → --file. Agent must not silently swap sources even
when the other looks 'better'.
---------
Co-authored-by: fangshuyu-768 <shuyufang768@outlook.com>
Allow drive +export to request bitable snapshots with --file-extension base and write them with a .base suffix.
Allow drive +import to accept .base files for bitable only, enforce the 20 MB size limit, and document the new examples and constraints.
Add unit tests for validation and size-limit coverage.
Change-Id: Ia13f5013913812df5fc600c43f90918de4ca6b39
Preserve fenced code blocks and balanced-parenthesis URLs when converting markdown to post elements. Add regression tests covering code-block URLs and wiki-style links.
Change-Id: I709a3daf3635402848c96b5122edfc67979ed1a4
When downloading message resources, the saved filename was always derived from
file_key (e.g. file_v2_abc123.xlsx), ignoring the original filename the
sender uploaded. This PR resolves filenames from the Content-Disposition
response header first, falling back to Content-Type-based extension inference
only when the header is absent.
Change-Id: I68b48cf428aa8aded4ad9d55fa042f9d68263c3a
Skill examples taught the pattern --markdown "## A\n\n- x\n- y",
which in bash double quotes is a literal backslash + n, not a
newline. lark-cli forwards the value byte-for-byte to MCP, so
the resulting Feishu doc renders "\n\n" as visible text. Agents
and users copy-pasting the examples reliably produced broken
docs.
Documentation-only fix (issue #580 Option 1, non-breaking):
- Replace 9 "...\n..." examples with multi-line quoted strings,
plus 1 single-quoted example that had the same bug inside
Markdown-block content
- Add a one-sentence warning callout at the top of each file
- Add a stdin/heredoc example in lark-doc-create.md for longer
content
- Leave existing $'...' ANSI-C examples untouched — those
already produce real newlines
No CLI behavior change. Byte-for-byte forwarding is standard
shell semantics; auto-interpreting \n (Option 2) would be a
breaking change and is intentionally not pursued.
Fixes#580
* feat(cmdutil): add X-Cli-Build header for CLI build classification
Adds X-Cli-Build (official / extended / unknown) so the gateway can distinguish official CLI from ISV-repackaged builds.
* test(cmdutil): lift coverage on build-kind classification
Extract classifyBuild as a pure helper so every branch (unknown / extended
main-path / extended credential / extended transport / extended fileio /
official) is reachable without mutating process-wide provider registries.
Also cover: isBuiltinProvider non-pointer values, BuildHeaderTransport
nil-Base fallback path, and fix the Name-spoof test so the test double
returns a value that actually mimics an ISV provider.
Coverage on PR-changed functions:
- classifyBuild: 100% (new)
- computeBuildKind: 61.5% -> 93.3%
- BuildHeaderTransport.RoundTrip: 80% -> 100%
Wrap the POST /drive/v1/permissions/:token/members/apply endpoint as a
user-only shortcut. --token accepts either a bare token or a document
URL, with type auto-inferred from the URL path (/docx/, /sheets/,
/base/, /bitable/, /file/, /wiki/, /doc/, /mindnote/, /minutes/,
/slides/); an explicit --type always wins. --perm is limited to view or
edit; full_access is rejected client-side to match the spec.
Classifier gains two domain-specific hints for the endpoint's newly
documented error codes: 1063006 (per-user-per-document quota of 5/day
reached) and 1063007 (document does not accept apply requests — covers
disallow-external-apply, already-has-access, and unsupported-type).
test(drive): add dry-run E2E for +apply-permission
Invoke the real CLI binary via clie2e.RunCmd under --dry-run and
parse the rendered request JSON with gjson to lock in method, URL
path (including the token segment), type query parameter (auto-inferred
for docx / sheet / slides URLs, taken from explicit --type for bare
tokens), perm body field, and remark presence/omission. A separate
test asserts --perm full_access is rejected by the enum validator
before reaching the server. Fake LARKSUITE_CLI_APP_ID / APP_SECRET /
BRAND are enough because dry-run short-circuits before any API call.
Update drive coverage.md to add a row and refresh metrics.
test(drive): isolate E2E dry-run subprocess from local CLI config
Set LARKSUITE_CLI_CONFIG_DIR to t.TempDir() in both +apply-permission
dry-run tests so the subprocess can't read a developer's real
credentials/profile instead of the fake env vars the tests inject.
test(drive): add E2E case that exercises URL inference override
Previous "bare token with explicit type wins over inference" row used a
bare token, which has no URL-derived type to override. Replace it with
a /docx/ URL + --type wiki combo that actually forces the explicit flag
to win over URL inference, and add a separate bare-token row to keep
the simpler path covered. Refresh coverage.md wording to match.
* fix(base): add default-table follow-up hint to base-create
* fix(base): route base-create hint to stderr
* fix(base): prefix base-create stderr tip
---------
Co-authored-by: kongenpei <kongenpei@users.noreply.github.com>
* fix: skip flag-completion registration outside completion path
Cobra keeps completion callbacks in a package-global map keyed by
*pflag.Flag with no removal path, so registrations made during Build()
outlive the command itself. Route all seven call sites through
cmdutil.RegisterFlagCompletion and enable registration only when the
invocation actually serves a __complete request.
Measured over 30 dropped Builds: ~202 KB / 2180 retained objects per
Build before, ~0 after.
Change-Id: I734d598a4c91a92c33b02e0f292f640cc0e224c6
The <<<<<<< HEAD marker was accidentally left in mail.md and SKILL.md
by commit cb301a3 (draft preview URL). Remove it.
Change-Id: I6e1d5c0c66761302a3c4ee1421a16961b666bd80
* feat(mail): add large attachment support via medias/upload API
When attachments would cause the EML to exceed the 25MB limit, they are
automatically uploaded to the mail attachment storage (medias/upload_all
with parent_type="email") and a download-link card is injected into the
HTML body, matching the desktop client's exportLargeFileArea style.
Key changes:
- Add classifyAttachments: EML-size-based splitting of normal vs oversized
- Add uploadLargeAttachments: upload via medias API with email MountPoint
- Add buildLargeAttachmentHTML: desktop-aligned card with CDN icons
- Add processLargeAttachments: unified entry point for all compose shortcuts
- Add LargeAttachmentHTML to emlbuilder.Builder for HTML block injection
- Fix 7bit line folding: use RFC 5322 limit (998) instead of incorrect 76
- Integrate into +draft-create, +forward, +reply, +reply-all
Known limitation: recipient access to large attachment links requires
backend support to register tokens with the draft (see progress doc).
Change-Id: If8d5938015cac8bc82de3ea3ff41022950f2571e
Co-Authored-By: AI
* refactor(mail): remove legacy size check, add 3GB limit, integrate +send
- Remove checkAttachmentSizeLimit (replaced by processLargeAttachments)
- Remove 25MB pre-check from validateComposeInlineAndAttachments so that
large files reach Execute where they are uploaded as large attachments
- Integrate processLargeAttachments into +send shortcut
- Add 3GB single file limit aligned with desktop client
- Clean up unused imports from helpers.go and helpers_test.go
Change-Id: Ie590ad2b58263c075f48b338959b8f5b3f912f85
Co-Authored-By: AI
* feat(mail): quote-aware HTML insertion, +draft-edit support, cleanup emlbuilder
- Add insertBeforeQuoteOrAppend: insert large attachment HTML before the
quote block (lark-mail-quote) instead of appending to body end, matching
desktop's exportLargeFileArea placement logic
- Add preprocessLargeAttachmentsForDraftEdit: intercept add_attachment
patch ops before draft.Apply, upload oversized files, inject HTML into
snapshot's HTML body Part directly. No changes to draft sub-package.
- Remove LargeAttachmentHTML field/setter/logic from emlbuilder — it was
business logic (quote-aware insertion) that doesn't belong in a generic
EML builder. processLargeAttachments now sets the full HTML body via
bld.HTMLBody() after merging the large attachment card at the right position.
- All compose shortcuts pass htmlBody to processLargeAttachments for
quote-aware insertion (composedHTMLBody for reply/forward, body for others).
Change-Id: If6e7ed7e77989ab9a8a41a93758f686d72ccf497
Co-Authored-By: AI
* fix(mail): align large attachment HTML IDs with desktop client
- Container ID: lark-mail-large-file-container → large-file-area (matching
desktop's MAIL_LARGE_FILE_CONTAINER constant)
- Item ID: lark-mail-large-file-item → large-file-item (matching
desktop's MAIL_LARGE_FILE_ITEM constant)
- Timestamp: truncate to 9 digits (matching TIMESTAMP_CUT_OUT_ID = 9)
- Refactor HTML generation to use template constants for readability
These IDs are used by the desktop client's BigAttachmentPlugin
([id^=large-file-area]) and the server's LargeFileRule to identify and
remove the HTML block when rendering the attachment card UI.
Change-Id: Ib5a77a1a3d60eeb3a05c585f2af0a5ddaacf887b
Co-Authored-By: AI
* docs(mail): document large attachment behavior in skill references
Update --attach parameter descriptions across all compose shortcuts
(+send, +reply, +reply-all, +forward, +draft-create, +draft-edit) to
describe automatic large attachment handling when EML exceeds 25 MB.
Change-Id: I8c30e390c127ea1119cb8c4b83ec636e41fbaf66
Co-Authored-By: AI
* fix(mail): pass signature-injected HTML to processLargeAttachments
When both --signature-id and large attachments are used, the htmlBody
passed to processLargeAttachments must include the already-injected
signature. Previously mail_send and mail_draft_create passed the
original body, causing processLargeAttachments to overwrite the
signature-injected HTML body when inserting the large attachment card.
Use composedHTMLBody variable (same pattern as reply/forward) to
capture the full processed HTML including signature.
Change-Id: I6be330776abca704b10cc3b8bfd5e20838e6e538
Co-Authored-By: AI
* fix(mail): skip draft.Apply when all ops consumed by large attachment preprocessing
When all patch ops are add_attachment targeting oversized files,
preprocessLargeAttachmentsForDraftEdit uploads them and removes the
ops from the patch. The resulting empty patch caused draft.Apply to
fail with "patch ops is required". Now skip Apply when no ops remain.
Change-Id: I8067a54b5f849fa519e8344a7eb10c48f58e54b8
Co-Authored-By: AI
* fix(mail): add X-Lms-Large-Attachment-Ids header in draft-edit large attachment flow
draft-edit's preprocessLargeAttachmentsForDraftEdit uploaded oversized files
and injected HTML cards but never wrote the X-Lms-Large-Attachment-Ids header
into the snapshot, so the mail server could not associate the attachments with
the draft. Merge new token IDs with any existing ones already in the snapshot.
Also extract the duplicated largeAttID struct and header name string into
package-level declarations.
Change-Id: Id256d948ec07e86296157436feefa3c2052af721
Co-Authored-By: AI
* fix(mail): i18n large attachment HTML text aligned with desktop client
Parameterize title and download text in large attachment HTML templates.
Chinese lang uses "来自Lark邮箱的超大附件"/"下载", others use
"Large file from Lark Mail"/"Download", matching desktop's i18n keys
Mail_Attachment_AttachmentFromFeishuMail and Mail_Attachment_Download.
Change-Id: I2aada8d52af41ae77dd7001d24d14e333f12066e
Co-Authored-By: AI
* fix(mail): insert large attachment card before quote wrapper, not inside nested quote
insertBeforeQuoteOrAppend matched id="lark-mail-quote" which can appear
deeply nested inside quoted content from previous replies in a thread.
This caused the card to be placed inside the quote area instead of before
it. Switch to matching the "history-quote-wrapper" class which is the
outermost quote container generated by the CLI.
Change-Id: I720b6d62d719613b411b7ed4b7820a1535bf14bd
Co-Authored-By: AI
* feat(mail): unify large attachment handling in +draft-edit with normal attachments
Extend +draft-edit so that large attachments behave like normal attachments
from the user's perspective: survive body edits, are listed in inspect
output, and are removed via the same remove_attachment op.
Code-wise:
- remove_attachment target now accepts token (for large attachments) in
addition to part_id / cid; priority part_id > cid > token.
- setBody / setReplyBody auto-preserve the large attachment card in the
HTML body, mirroring how normal attachments (MIME parts) survive body
edits. Detection checks only the user-authored region of the value so
cards inside an appended quote block (from the original quoted message)
are not mistaken for user-supplied cards.
- --inspect returns large_attachments_summary (token, filename, size) by
parsing the X-Lms-Large-Attachment-Ids header and the HTML card DOM.
- Well-known Lark HTML/header constants (LargeAttachmentIDsHeader,
LargeFileContainerIDPrefix, LargeFileItemID, LargeAttachmentTokenAttr)
moved to the draft package alongside QuoteWrapperClass; the mail package
consumes them.
- Shared helpers FindHTMLBodyPart and InsertBeforeQuoteOrAppend exported
from the draft package; mail package switched to consume them, removing
local duplicates.
Skill reference (lark-mail-draft-edit.md) updated: three locator fields by
attachment type, unified remove_attachment examples, set_body behavior.
Change-Id: Ic064d1a8df0edf1cef6069cd44ec2a7534cd2182
Co-Authored-By: AI
* fix(mail): place signature before large attachment card consistently
When inserting a signature into a draft that already has a large
attachment card, the signature was placed after the card, diverging from
the compose-time layout where the order is [user][sig][card][quote].
Root cause: insertSignatureOp split only at the quote block, so the
"user region" side inadvertently included the card.
Centralize signature placement in draft.PlaceSignatureBeforeSystemTail,
which splits at the earliest system-managed element (card or quote,
whichever comes first). Both edit-time insertSignatureOp and compose-time
injectSignatureIntoBody now share this single source of truth, removing
the duplicated HTML splicing logic.
Change-Id: I234bfebaaa31a32731ebbaa78c6596a72618b7c5
Co-Authored-By: AI
* fix(mail): auto-preserve signature in set_body and set_reply_body
Previously set_body / set_reply_body replaced the entire HTML body,
silently dropping the signature block. The "replace whole body" semantic
treated signature as user-authored content, which is inconsistent with
how attachments (normal + large) and quote blocks survive body edits —
signature is a system-managed element managed via insert_signature /
remove_signature ops.
Unify the mental model: body-edit ops replace user-authored content
only; signature, large attachment card, normal attachments, and (for
set_reply_body) quote block are all auto-preserved. Users can override
by including equivalents in value, or explicitly delete via dedicated
ops (remove_signature, remove_attachment).
- Add ExtractSignatureBlock helper (symmetric to RemoveSignatureHTML).
- Rename autoPreserveLargeAttachmentCard to
autoPreserveSystemManagedRegions; extract and inject both sig and card
from old body, respecting user-supplied equivalents in value's
user-authored region.
- Update skill doc and patch template notes to reflect the new
semantics consistently.
Change-Id: I96660d2ff06a6c9cdf1b86793c2d89cf9cb09ffe
Co-Authored-By: AI
* fix(mail): use brand-aware display name in large attachment card title
The title "Large file from Lark Mail" / "来自Lark邮箱的超大附件" hard-coded
"Lark" regardless of brand. The desktop client switches between
"Feishu"/"飞书" and "Lark" based on the APP_DISPLAY_NAME i18n
substitution.
Add brandDisplayName(brand, lang) helper:
- BrandLark → "Lark"
- BrandFeishu → "飞书" (zh) / "Feishu" (en)
Applied to title in buildLargeAttachmentHTML, aligning with the icon CDN
and download URL, which already branch on brand.
Change-Id: I06258b9982b6280a2230193d90a6a88884e10aa3
Co-Authored-By: AI
* style(mail): apply gofmt
CI fast-gate check flagged gofmt-unformatted files. Run gofmt -w on
touched mail files only.
Change-Id: Iec690dc63adfaa54b8f7c85ab5b3ca035476ddbd
* fix(mail): address review feedback on large attachment PR
- Strip <html><head><body> wrapper from xhtml.Render output in
removeLargeFileItemFromHTML to avoid polluting the HTML body
- Reject plain-text messages with oversized attachments instead of
silently losing the body content
- Fix attachment count limit in skill doc (100 → 250)
- Remove unused fio/attachFlag params from validateComposeInlineAndAttachments
- Add token escaping test for large attachment HTML builder
Change-Id: Ie589a1f1d204b0aeebc4486b16bb435041793ceb
Co-Authored-By: AI
* fix(mail): recognize server-format X-Lark-Large-Attachment header in draft-edit
When a draft with large attachments is created by the desktop client,
the server returns X-Lark-Large-Attachment (with file_key/file_name/
file_size fields) instead of the CLI-written X-Lms-Large-Attachment-Ids.
Previously CLI only recognized its own header, causing existing large
attachments to be silently dropped when the draft was edited.
- Parse both header formats via IsLargeAttachmentHeader and unified
largeAttHeaderEntry struct
- Convert server-format entries to CLI-format on save so the server
can process the update
- Fix inline attachment classification: require non-empty CID to
classify as inline image (large attachments may have is_inline=true
but no CID)
Change-Id: Ie7def4fc5923d2cf3446eedfbca4fd8cae44bfac
Co-Authored-By: AI
* fix(mail): skip large attachments in forward URL validation
Large attachments do not have download URLs since they are referenced
by token, not embedded in the EML. Validate only normal attachments
to avoid false "missing download URL" errors when forwarding messages
that contain expired or token-based large attachments.
Change-Id: Ibe3f45390cd3b3cbe6ddd15961dcda4f17aefe4f
Co-Authored-By: AI
* fix(mail): classify forwarded original attachments for large attachment upload
Previously, all original attachments were unconditionally embedded in
the EML before user attachments were processed for large attachment
upload. When original + user attachments together exceeded the 25 MB
EML limit, the build would fail.
Now all attachments (original + user-added) are classified together
via classifyAttachments. Original attachments that push the EML over
the limit are re-uploaded as large attachments with download cards,
matching the compose/reply flow behavior.
Also refactors uploadLargeAttachmentBytes to reuse the shared
common.UploadDriveMediaAll utility (via new Reader field on the
config struct) instead of duplicating the upload logic, and replaces
bare fmt.Errorf with output.ErrValidation for user input errors.
Change-Id: I98d4ad8960cd68e38765b05c94f7786d6a8444c8
Co-Authored-By: AI
* fix(mail): normalize large attachment header on draft edit to prevent loss
Server returns X-Lark-Large-Attachment header on draft readback, but only
recognizes X-Lms-Large-Attachment-Ids on write. Without normalization,
editing a draft with existing large attachments (e.g. adding a small
attachment) would send back the server-format header unchanged, causing
the server to drop the large attachment association.
Add normalizeLargeAttachmentHeader() at the entry of
preprocessLargeAttachmentsForDraftEdit to convert server-format headers
to CLI format before any processing or early return.
Change-Id: Id99a46f29015a32921bfb72a003f766c397787e1
Co-Authored-By: AI
* fix(mail): extract large attachment card from quote on forward
When forwarding a message that contains large attachments, the original
message's download card (large-file-area div) was left inside the
forward quote block. Extract it and place it in the main body area
(after signature, before quote), matching the desktop client behavior.
Change-Id: Iebede35cdf4ed0f65b72bce28ffb18af21ddf668
Co-Authored-By: AI
* fix(mail): use octet-stream for re-embedded attachments and file-based large upload on forward
- Use application/octet-stream instead of original content type when
re-embedding downloaded attachments in forward EML. Prevents the mail
server from treating image/* attachments as inline parts.
- Replace in-memory uploadLargeAttachmentBytes with temp-file-based
uploadLargeAttachments for oversized original attachments. This
enables multipart upload for files >20MB which the single-part API
does not support.
Change-Id: Ib02add5710e8b052e47b513ed3d9a688e0f98212
Co-Authored-By: AI
* fix(mail): address PR review — blocked extension bypass, index-based op filtering, plain-text draft guard
1. Move CheckBlockedExtension into statAttachmentFiles so oversized
attachments are validated before classification, covering compose,
draft-edit, and forward paths.
2. Replace path-based oversized op filtering with SourceIndex-based
filtering in preprocessLargeAttachmentsForDraftEdit to avoid
incorrectly removing duplicate-path normal ops.
3. Add HTML body preflight in preprocessLargeAttachmentsForDraftEdit
before uploading, so plain-text-only drafts fail early instead of
silently producing a draft with tokens but no download card.
Change-Id: Ib8771812f50a18f00a40e50149b028b8aaa101fe
Co-Authored-By: AI
* fix(mail): preserve original content type for normal forwarded attachments
The octet-stream override was only needed for the large attachment
upload path (to prevent image/* from being treated as inline by the
drive API). Normal attachments embedded in the EML should retain their
original MIME type so recipients can preview/open them correctly.
Change-Id: Ie40b7c362524a3b82255b58e9bcfd770eacfe911
Co-Authored-By: AI
* fix(mail): reconstruct missing large attachment HTML cards on draft edit
The server strips HTML download cards from the EML body when storing
drafts, so every draft read-back (regardless of creator) lacks them.
Add ensureLargeAttachmentCards which runs before header normalization,
compares server-format header tokens against existing HTML cards via
data-mail-token, and rebuilds only the missing ones. This ensures
external recipients see download links after draft-edit → send.
Also exports ParseLargeAttachmentSummariesFromHeader and
ParseLargeAttachmentItemsFromHTML from the draft package for
cross-package use.
Change-Id: I9cb0f47a9f4582909de24984d9a9f6e366521e62
Co-Authored-By: AI
* feat(mail): support large attachments in plain-text emails
Previously large attachments required an HTML body for the download card.
Now plain-text emails (--plain-text or text/plain-only drafts) get download
info appended as structured text (title + filename + size + URL), with
i18n and brand awareness matching the HTML card.
Changes:
- Add buildLargeAttachmentPlainText and injectLargeAttachmentTextIntoSnapshot
- Add FindTextBodyPart in draft/projection.go
- Update processLargeAttachments to accept textBody parameter
- Update ensureLargeAttachmentCards to handle text/plain body reconstruction
- Update preprocessLargeAttachmentsForDraftEdit to allow text/plain drafts
- Update all callers (send, draft-create, reply, reply-all, forward)
Change-Id: I3b375e2ff34697eeb73a3768ace6d577d1bead3e
Co-Authored-By: AI
* fix(mail): FindBodyPart skips attachment-disposition parts; update skill docs
FindHTMLBodyPart and FindTextBodyPart now skip parts with
Content-Disposition: attachment, preventing .txt/.html file attachments
from being mistakenly treated as the email body.
Also update all lark-mail skill reference docs to reflect that large
attachments now work in both HTML (download card) and plain-text
(download link text) modes.
Change-Id: I1e6da4fd614217dff61304212304b5fd80c8246c
Co-Authored-By: AI
* fix(mail): fix origIdx mismatch, predictable temp files, and attachment count on forward
- Use SourceIndex instead of linear origIdx counter so classifyAttachments
reordering does not cause content mismatch between normal/oversized loops
- Use os.CreateTemp for temp files instead of predictable names in CWD
- Include original large attachment count in totalCount limit check
Change-Id: Ide5dce14b1efc672687800d77c3853f15dfc191b
Co-Authored-By: AI
* fix(mail): use composed body size and source inline bytes in EML size estimation
estimateEMLBaseSize was using len(body) (raw --body flag) instead of the
actual composed body (which includes quotes, signatures, forward headers).
Source inline images downloaded from the original message were also not
counted. This could cause borderline attachments to be misclassified.
- Use len(composedHTMLBody) + len(composedTextBody) for body size
- Return total downloaded bytes from addInlineImagesToBuilder and pass
as extraBytes to estimateEMLBaseSize
- Fix applied to all compose shortcuts: send, draft-create, reply,
reply-all, forward
Change-Id: Ibe6c44e22d40ac51f0a4652d279e66bd92330723
Co-Authored-By: AI
* fix(mail): merge large attachment items into single container on draft edit
When draft-edit had both set_body and add_attachment (oversized), the
ensureLargeAttachmentCards and preprocessLargeAttachmentsForDraftEdit
each created independent large-file-area containers. The subsequent
set_body's autoPreserveSystemManagedRegions only captured the first
container via SplitAtLargeAttachment, discarding the second one.
Fix: injectLargeAttachmentHTMLIntoSnapshot now detects an existing
large-file-area container and appends new items inside it instead of
creating a new container, matching the desktop client's single-container
behavior.
Change-Id: I3d701683053842f1d7bdad34fc4b2ef26ede784e
Co-Authored-By: AI
* fix(mail): strip large attachment card from reply/reply-all quote
Reply and reply-all should not carry over the original email's large
attachment HTML card into the quoted block. Extract the shared
stripLargeAttachmentCard helper (also used by forward) that removes
the card from orig.bodyRaw before quote construction.
- Reply/reply-all: card is discarded (not re-inserted)
- Forward: card is moved to body area before the quote (unchanged)
Change-Id: I5399bb901c120206c7c045bed107f7d68be23bb1
Co-Authored-By: AI
* fix(mail): skip invalid attachments on forward instead of blocking
When forwarding a message with deleted/expired attachments, the forward
flow now automatically removes them instead of either blocking (normal
attachments) or silently including dead references (large attachments).
- Propagate failed_ids from fetchAttachmentURLs into composeSourceMessage
- Skip failed attachments in the forward download loop with a warning
- Remove corresponding large attachment HTML card items from the body
- Extend itemContainsToken to match server-generated href?token= format
Change-Id: I9c0096dcbe96f1d61caa0f6f0b2f8b738fdfa66b
Co-Authored-By: AI
* fix(mail): restore dry-run file preflight and reserve card overhead in classifier
1. Restore file existence and blocked-extension checks in
validateComposeInlineAndAttachments so --dry-run surfaces local
path errors before Execute.
2. Reserve 3KB per oversized file in classifyAttachments to account
for the HTML card / plain-text block injected after classification.
Change-Id: Ib48a75f86a50298413c1f9ab8226e583c0161a8c
Co-Authored-By: AI
* fix(mail): revert classifier overhead reserve for simplicity
The 3KB-per-oversized-file reserve in classifyAttachments addressed
a boundary case that is practically impossible to trigger (requires
Normal attachments to fill to within a few KB of 25MB). Remove it
to keep the classifier simple.
Change-Id: I5148f14ecca1a0dee677a1a2c60ec4efab160ea8
Co-Authored-By: AI
* style(mail): fix gofmt indentation in draft create tests
Change-Id: Ib41aa22f94144f2d47b12675d444aa43cb333a88
Co-Authored-By: AI
* fix(mail): remove temp files in forward, use in-memory upload instead
Replace os.CreateTemp/os.WriteFile/os.Remove with in-memory Data field
on attachmentFile, conforming to the project's forbidigo rule against
temp files in shortcuts. Also remove dead uploadLargeAttachmentBytes.
Change-Id: Ic26e4025eebfa1bac3948438ef185ff3e2f15abb
Co-Authored-By: AI
* test(mail): add tests for validateComposeInlineAndAttachments and fileTypeIcon
Covers all branches: inline+plain-text conflict, inline+non-HTML body,
missing file, blocked extension, valid pass-through, and all file type
icon mappings.
Change-Id: I8b81c1b34010a9ecb7153462a5524e3d7b171de2
Co-Authored-By: AI
* test(mail): improve coverage for large attachment and draft edit functions
Add tests for snapshotEMLBaseSize, flattenSnapshotParts, estimateEMLBaseSize,
normalizeLargeAttachmentHeader, processLargeAttachments error paths,
preprocessLargeAttachmentsForDraftEdit early-return paths, inject edge cases,
buildLargeAttachmentItems, statAttachmentFiles edge cases, and
prettyDraftAddresses.
Change-Id: Ie661e6ebea63512864d97e20135dd89cb9e9304e
Co-Authored-By: AI