Skip to content

refactor: migrate mail shortcuts to FileIO#356

Merged
tuxedomm merged 5 commits intomainfrom
feat/fileio-migrate-mail
Apr 9, 2026
Merged

refactor: migrate mail shortcuts to FileIO#356
tuxedomm merged 5 commits intomainfrom
feat/fileio-migrate-mail

Conversation

@tuxedomm
Copy link
Copy Markdown
Collaborator

@tuxedomm tuxedomm commented Apr 9, 2026

Summary

  • Migrate all mail shortcut file operations (vfs.* calls) to use runtime.FileIO() abstraction
  • Update helpers.go, emlbuilder/builder.go, draft/patch.go, and all mail command files (mail_draft_create.go, mail_draft_edit.go, mail_forward.go, mail_reply.go, mail_reply_all.go, mail_send.go)
  • Extract DraftCtx from DraftSnapshot to separate runtime dependencies (FileIO) from pure data model
  • Preserve original error messages and validation order during migration

Test plan

Automated tests

  • go test ./shortcuts/mail/... passes
  • go test ./shortcuts/mail/draft/... passes
  • go test ./shortcuts/mail/emlbuilder/... passes
  • golangci-lint run ./shortcuts/mail/... clean (6 pre-existing issues, none introduced by this PR)

Manual testing (CLI built from branch HEAD: ed1c3ee)

# Scenario Result
1 +draft-create with --attach
2 +draft-create with inline image (<img src="local.png">)
3 +draft-edit --inspect
4 +draft-edit --patch-file with add_attachment (DraftCtx core path)
5 +reply (draft only, no send)
6 +forward
7 +reply with --attach
8 Error: non-existent attachment file
9 +forward with inline image
10 +draft-edit patch set_subject + set_body (non-file ops)

Not tested: +send / --confirm-send (requires mail:user_mailbox.message:send scope, not available)

Summary by CodeRabbit

Release Notes

  • Refactor
    • Updated file handling architecture across mail operations including draft creation, editing, forwarding, replies, and sending. Restructured attachment and inline image processing components along with validation and composition utilities.

tuxedomm added 3 commits April 9, 2026 12:44
- DraftSnapshot.FIO: inject FileIO into draft snapshot for patch ops
  (addAttachment, loadAndAttachInline, replaceInline)
- emlbuilder.Builder.fio: inject via WithFileIO(), readFile uses FileIO.Open
- mail_draft_edit: loadPatchFile uses runtime.FileIO().Open
- helpers: checkAttachmentSizeLimit takes fio param, uses FileIO.Stat
- validateComposeInlineAndAttachments: pass fio through to size check
- All mail entry points (send/reply/reply_all/forward/draft_create):
  pass runtime.FileIO() to builder and size limit checks

Change-Id: I580b126a970c57b4ccfcf13dab4d6aacbe255de9
- addAttachment/replaceInline: distinguish path validation errors
  (wrapped with context) from stat failures (bare error), matching
  original SafeInputPath + vfs.Stat two-step behavior
- checkAttachmentSizeLimit: distinguish "unsafe attachment path" (path
  validation) from "failed to stat attachment" (file not found/perm)

Change-Id: I6e632c949aa9803a05ef63f529c3db9dfa8ab839
…ration

- addAttachment: restore original order (path validation via Stat before
  extension check) so path traversal errors are reported before blocked
  extension errors
- DraftSnapshot.FIO comment: remove "nil falls back" claim; nil panics
- Builder.fio comment: same fix

Change-Id: I21ff209987c72704f2302ed3481c58e2a4256764
@github-actions github-actions Bot added domain/mail PR touches the mail domain size/L Large or sensitive change across domains or core paths labels Apr 9, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 9, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 69731893-cc5d-4ae1-86f8-a5e184c98c5a

📥 Commits

Reviewing files that changed from the base of the PR and between ed1c3ee and 1c3be98.

📒 Files selected for processing (1)
  • shortcuts/mail/draft/patch.go

📝 Walkthrough

Walkthrough

Introduce a runtime dependency container DraftCtx holding fileio.FileIO, thread it through draft patch application and EML building, and replace direct vfs/SafeInputPathfile ops withFileIO` Open/Stat/Read across mail shortcuts and tests.

Changes

Cohort / File(s) Summary
Draft context & patch
shortcuts/mail/draft/model.go, shortcuts/mail/draft/patch.go
Add DraftCtx (FIO fileio.FileIO); change Apply signature to Apply(dctx *DraftCtx, ...) and thread dctx into apply/post-process functions; replace vfs/SafeInputPathusage withdctx.FIO.Stat/Open/read paths and propagate fileio.ErrPathValidation`.
Draft tests & acceptance
shortcuts/mail/draft/.../*.go (e.g. patch_test.go, patch_attachment_test.go, patch_body_test.go, patch_header_test.go, patch_inline_resolve_test.go, patch_recipient_test.go, serialize_*.go, acceptance_test.go)
Update test call sites to supply &DraftCtx{FIO: testFIO} into Apply and attachment helpers; add/import localfileio test FIO where needed.
EML builder & tests
shortcuts/mail/emlbuilder/builder.go, shortcuts/mail/emlbuilder/builder_test.go
Add injectable fio fileio.FileIO to Builder with WithFileIO(fio); switch internal read helper to use provided fio.Open/read and update tests to construct builders with WithFileIO(testFIO).
Mail helpers & tests
shortcuts/mail/helpers.go, shortcuts/mail/helpers_test.go
Change checkAttachmentSizeLimit and validateComposeInlineAndAttachments to accept a fileio.FileIO argument; use fio.Stat and return distinct errors for fileio.ErrPathValidation; update tests to pass LocalFileIO.
Mail commands
shortcuts/mail/*_create.go, *_edit.go, *_forward.go, *_reply*.go, *_send.go
Pass runtime.FileIO() into validation, size checks, and EML builder initialization (emlbuilder.New().WithFileIO(runtime.FileIO())); update patch-file loading to use runtime.FileIO().Open + io.ReadAll.

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Runtime
  participant DraftPkg
  participant FileIO

  CLI->>Runtime: execute mail command (attach/inline/patch)
  Runtime->>DraftPkg: create DraftCtx{FIO: Runtime.FileIO()}
  CLI->>DraftPkg: Apply(dctx, snapshot, patch)
  DraftPkg->>FileIO: Stat/Open(path) via dctx.FIO
  FileIO-->>DraftPkg: file handle / bytes or ErrPathValidation / error
  DraftPkg->>DraftPkg: process attachment/inline, create parts
  DraftPkg-->>Runtime: return mutated snapshot / error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • liangshuo-1
  • kongenpei
  • chanthuang

Poem

"I nibble bytes beside the log,
A DraftCtx basket in my fog,
FileIO trails I gently trace,
Inline snacks find their place,
Hopping mail along the code-lined bog 🐇"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'refactor: migrate mail shortcuts to FileIO' accurately summarizes the main change: migrating mail shortcut file operations to use the FileIO abstraction instead of vfs calls.
Description check ✅ Passed The PR description includes a clear summary of the changes, detailed test plan with both automated tests and manual testing scenarios, and explains the architectural decision to extract DraftCtx. All required template sections are present and well-filled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/fileio-migrate-mail

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 9, 2026

Greptile Summary

This PR migrates all mail shortcut file I/O from the vfs package to runtime.FileIO(), and introduces DraftCtx to cleanly separate runtime dependencies (FileIO) from the pure data model (DraftSnapshot). The refactor is structurally sound, and all call sites correctly wire in FileIO via WithFileIO or DraftCtx.

One new concern: the Builder.fio field added in this PR is left unguarded. Calling AddFileAttachment, AddFileInline, or AddFileOtherPart on a builder created by New() without WithFileIO(...) will panic at fio.Open(path) in readFile, bypassing the builder's established error-accumulation pattern.

Confidence Score: 4/5

Safe to merge once the Builder.fio nil-guard is addressed; all production call sites already use WithFileIO correctly.

The refactor is clean and all existing callers are correctly wired. One P1 remains: AddFileAttachment/AddFileInline/AddFileOtherPart will panic rather than accumulate an error when WithFileIO is omitted, violating the builder's own contract. Concerns about DraftCtx.FIO nil guard and error-wrapping inconsistency were raised in prior review rounds and remain outstanding.

shortcuts/mail/emlbuilder/builder.go — AddFileAttachment, AddFileInline, AddFileOtherPart need nil guards on b.fio

Vulnerabilities

No security concerns identified. The migration from vfs to FileIO preserves existing path-validation semantics (relative-path enforcement, blocked-extension checks). No new trust boundaries are crossed and no credentials are exposed.

Important Files Changed

Filename Overview
shortcuts/mail/emlbuilder/builder.go Migrated file reads from vfs to injected FileIO via WithFileIO; AddFile* methods call readFile(b.fio, …) but lack a nil guard on b.fio, causing a panic instead of an error when WithFileIO is not called.
shortcuts/mail/draft/patch.go Core patch-apply logic migrated to DraftCtx/FileIO; checkBlockedExtension correctly precedes FIO.Stat in addAttachment; error-context wrapping is inconsistent between loadAndAttachInline (all errors wrapped) and addAttachment/replaceInline (bare errors).
shortcuts/mail/draft/model.go Introduces DraftCtx to carry FIO dependency separately from DraftSnapshot; the FIO field is documented as required but not guarded in Apply or its callees.
shortcuts/mail/mail_draft_edit.go DraftCtx correctly wired with runtime.FileIO(); loadPatchFile migrated to use FileIO.Open; no issues.
shortcuts/mail/helpers.go checkAttachmentSizeLimit and validateComposeInlineAndAttachments correctly accept fileio.FileIO; all call sites pass runtime.FileIO(); clean migration.
shortcuts/mail/emlbuilder/builder_test.go Test suite updated to use WithFileIO(testFIO) for all AddFile* paths; comprehensive coverage of blocked extensions and allowed formats.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Mail Command] --> B[runtime.FileIO]
    B --> C[DraftCtx with FIO]
    B --> D[emlbuilder.New with WithFileIO]

    C --> E[draft.Apply dctx + snapshot + patch]
    E --> F{op type}
    F -->|add_attachment| G[addAttachment]
    F -->|add_inline / replace_inline| H[loadAndAttachInline / replaceInline]
    F -->|set_body / set_reply_body| I[postProcessInlineImages]
    I --> H

    G --> K[dctx.FIO.Stat + Open]
    H --> K

    D --> L[AddFileAttachment]
    D --> M[AddFileInline / AddFileOtherPart]
    L --> N[readFile via b.fio]
    M --> N

    style N fill:#fbb,stroke:#f00
    style K fill:#bfb,stroke:#0a0
Loading

Comments Outside Diff (1)

  1. shortcuts/mail/emlbuilder/builder.go, line 428-442 (link)

    P1 Nil fio panics instead of accumulating an error

    AddFileAttachment, AddFileInline, and AddFileOtherPart all forward to readFile(b.fio, path). If WithFileIO was never called, b.fio is a nil interface, and fio.Open(path) inside readFile will panic at runtime rather than return a stored error. This breaks the builder's own error-accumulation contract — Build() is supposed to be the only call site that can panic-equivalent-fail.

    A nil guard at the top of each AddFile* method would be consistent with the pattern already used for b.err:

    func (b Builder) AddFileAttachment(path string) Builder {
        if b.err != nil {
            return b
        }
        if b.fio == nil {
            b.err = fmt.Errorf("emlbuilder: FileIO not set; call WithFileIO before AddFileAttachment")
            return b
        }
        // ...
    }

    The same guard is needed in AddFileInline (line 487) and AddFileOtherPart (line 546).

Reviews (3): Last reviewed commit: "fix: restore original validation order i..." | Re-trigger Greptile

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 9, 2026

🚀 PR Preview Install Guide

🧰 CLI update

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

🧩 Skill update

npx skills add larksuite/cli#feat/fileio-migrate-mail -y -g

Comment thread shortcuts/mail/draft/patch.go Outdated
Comment thread shortcuts/mail/draft/patch.go Outdated
Comment thread shortcuts/mail/draft/model.go
Move fileio.FileIO out of DraftSnapshot (pure data model) into a
separate DraftCtx struct, keeping data and runtime concerns decoupled.
Apply and internal file-operation functions now receive *DraftCtx as
an explicit parameter.

Change-Id: Ibabb77c389f75db8cc92d3558a350774e90d1ce1
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: 2

🧹 Nitpick comments (1)
shortcuts/mail/emlbuilder/builder.go (1)

76-102: Consider adding a nil-guard or documenting the requirement for fio.

The fio field comment states "must be set before AddFile* calls", but if a caller forgets to call WithFileIO(), the code will panic with a nil pointer dereference in readFile when fio.Open(path) is called.

Options:

  1. Return an explicit error in AddFile* methods if b.fio == nil
  2. Accept current behavior since all call sites in this codebase use runtime.FileIO() which is guaranteed non-nil

Given that retrieved learnings confirm runtime.FileIO() is always non-nil in this package's execution context, this is acceptable. However, if emlbuilder is intended for use outside this context, a defensive check would be safer.

🛡️ Optional: Add explicit nil check for clearer error
 func readFile(fio fileio.FileIO, path string) ([]byte, error) {
+	if fio == nil {
+		return nil, fmt.Errorf("attachment %q: FileIO not configured (call WithFileIO before AddFile*)", path)
+	}
 	f, err := fio.Open(path)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/emlbuilder/builder.go` around lines 76 - 102, The Builder's
fio field can be nil and will cause a panic when readFile calls fio.Open(path);
add a defensive nil-guard: in each public AddFile* method (and/or readFile)
check if b.fio == nil and return a clear error (or set b.err) instead of
dereferencing, or alternately update the Builder docs/comment near
fio/WithFileIO to explicitly state WithFileIO must be called and that fio is
required; locate fio, WithFileIO, readFile and the AddFile* methods to implement
the chosen fix so callers get an explicit error rather than a nil pointer panic.
🤖 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/draft/patch.go`:
- Around line 497-504: When reading attachments/inlines using snapshot.FIO.Open
followed by io.ReadAll (the block with snapshot.FIO.Open and io.ReadAll),
preserve the path context on errors by wrapping the returned error with a
descriptive message that includes the variable path (e.g., use fmt.Errorf("read
attachment %s: %w", path, err) or errors.Wrapf). Apply the same pattern in the
other occurrence (the block around lines 619-626) and ensure both the Open error
and the ReadAll error include the path so callers see which attachment/inline
failed; reference the snapshot.FIO.Open + io.ReadAll sequence and
loadAndAttachInline behavior for consistency.

In `@shortcuts/mail/mail_draft_edit.go`:
- Around line 266-275: The io.ReadAll error in loadPatchFile should preserve the
same "--patch-file" context as the Open error; replace the raw return of err
after io.ReadAll with a wrapped error like fmt.Errorf("--patch-file %q: %w",
path, err) so callers still see the flag/path in failure messages. Update the
error return in loadPatchFile (around the io.ReadAll handling) to wrap the read
error using the same format string and include the path variable.

---

Nitpick comments:
In `@shortcuts/mail/emlbuilder/builder.go`:
- Around line 76-102: The Builder's fio field can be nil and will cause a panic
when readFile calls fio.Open(path); add a defensive nil-guard: in each public
AddFile* method (and/or readFile) check if b.fio == nil and return a clear error
(or set b.err) instead of dereferencing, or alternately update the Builder
docs/comment near fio/WithFileIO to explicitly state WithFileIO must be called
and that fio is required; locate fio, WithFileIO, readFile and the AddFile*
methods to implement the chosen fix so callers get an explicit error rather than
a nil pointer panic.
🪄 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: 0d253e5b-5926-4381-bdf0-4279e06c4f34

📥 Commits

Reviewing files that changed from the base of the PR and between af83e54 and 0785663.

📒 Files selected for processing (14)
  • shortcuts/mail/draft/model.go
  • shortcuts/mail/draft/patch.go
  • shortcuts/mail/draft/patch_attachment_test.go
  • shortcuts/mail/draft/patch_test.go
  • shortcuts/mail/emlbuilder/builder.go
  • shortcuts/mail/emlbuilder/builder_test.go
  • shortcuts/mail/helpers.go
  • shortcuts/mail/helpers_test.go
  • shortcuts/mail/mail_draft_create.go
  • shortcuts/mail/mail_draft_edit.go
  • shortcuts/mail/mail_forward.go
  • shortcuts/mail/mail_reply.go
  • shortcuts/mail/mail_reply_all.go
  • shortcuts/mail/mail_send.go

Comment thread shortcuts/mail/draft/patch.go Outdated
Comment thread shortcuts/mail/mail_draft_edit.go
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.

♻️ Duplicate comments (1)
shortcuts/mail/draft/patch.go (1)

497-505: ⚠️ Potential issue | 🟡 Minor

Preserve path context for Open/ReadAll errors in attachment/inline updates.

These branches still return raw I/O errors, which can hide which input path failed. Please wrap both Open and io.ReadAll errors with the file path, consistent with the surrounding contextual errors.

🛠️ Suggested patch
 func addAttachment(dctx *DraftCtx, snapshot *DraftSnapshot, path string) error {
@@
 	f, err := dctx.FIO.Open(path)
 	if err != nil {
-		return err
+		return fmt.Errorf("attachment %q: %w", path, err)
 	}
 	defer f.Close()
 	content, err := io.ReadAll(f)
 	if err != nil {
-		return err
+		return fmt.Errorf("attachment %q: %w", path, err)
 	}
@@
 func replaceInline(dctx *DraftCtx, snapshot *DraftSnapshot, partID, path, cid, fileName, contentType string) error {
@@
 	f, err := dctx.FIO.Open(path)
 	if err != nil {
-		return err
+		return fmt.Errorf("inline image %q: %w", path, err)
 	}
 	defer f.Close()
 	content, err := io.ReadAll(f)
 	if err != nil {
-		return err
+		return fmt.Errorf("inline image %q: %w", path, err)
 	}

Also applies to: 619-627

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

In `@shortcuts/mail/draft/patch.go` around lines 497 - 505, The Open and ReadAll
error returns in the attachment/inline update code should include the file path
for context; locate the calls to dctx.FIO.Open(path) and io.ReadAll(f) and
change their error returns to wrap the underlying error with the path (e.g.,
using fmt.Errorf or errors.Wrapf) so the returned error reads like "open/read
<path>: <original error>" and do the same for the analogous block around the
later occurrence (the block at the other reported lines handling attachments).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@shortcuts/mail/draft/patch.go`:
- Around line 497-505: The Open and ReadAll error returns in the
attachment/inline update code should include the file path for context; locate
the calls to dctx.FIO.Open(path) and io.ReadAll(f) and change their error
returns to wrap the underlying error with the path (e.g., using fmt.Errorf or
errors.Wrapf) so the returned error reads like "open/read <path>: <original
error>" and do the same for the analogous block around the later occurrence (the
block at the other reported lines handling attachments).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3456c035-b4c4-4dd6-b2f5-af62fc277c1f

📥 Commits

Reviewing files that changed from the base of the PR and between 0785663 and ed1c3ee.

📒 Files selected for processing (12)
  • shortcuts/mail/draft/acceptance_test.go
  • shortcuts/mail/draft/model.go
  • shortcuts/mail/draft/patch.go
  • shortcuts/mail/draft/patch_attachment_test.go
  • shortcuts/mail/draft/patch_body_test.go
  • shortcuts/mail/draft/patch_header_test.go
  • shortcuts/mail/draft/patch_inline_resolve_test.go
  • shortcuts/mail/draft/patch_recipient_test.go
  • shortcuts/mail/draft/patch_test.go
  • shortcuts/mail/draft/serialize_golden_test.go
  • shortcuts/mail/draft/serialize_test.go
  • shortcuts/mail/mail_draft_edit.go
✅ Files skipped from review due to trivial changes (1)
  • shortcuts/mail/draft/patch_attachment_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • shortcuts/mail/draft/model.go
  • shortcuts/mail/mail_draft_edit.go

Move checkBlockedExtension back before FIO.Stat to match the original
SafeInputPath → checkBlockedExtension → Stat order. Also remove unused
errors and fileio imports.

Change-Id: I42e726be30409d03a16bb7306625732fd103d8b9
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/L Large or sensitive change across domains or core paths

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants