diff --git a/shortcuts/mail/draft/acceptance_test.go b/shortcuts/mail/draft/acceptance_test.go
index f86c09118..83fad1781 100644
--- a/shortcuts/mail/draft/acceptance_test.go
+++ b/shortcuts/mail/draft/acceptance_test.go
@@ -11,7 +11,7 @@ func TestAcceptanceReplyDraftSubjectOnly(t *testing.T) {
originalInline := findPart(snapshot.Body, "1.2")
originalAttachment := findPart(snapshot.Body, "1.3")
- if err := Apply(snapshot, Patch{
+ if err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_subject", Value: "Reply updated"}},
}); err != nil {
t.Fatalf("Apply() error = %v", err)
@@ -46,7 +46,7 @@ func TestAcceptanceReplyDraftSubjectOnly(t *testing.T) {
func TestAcceptanceHTMLInlineReplaceHTML(t *testing.T) {
snapshot := mustParseFixtureDraft(t, mustReadFixture(t, "testdata/html_inline_draft.eml"))
- if err := Apply(snapshot, Patch{
+ if err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "replace_body", BodyKind: "text/html", Selector: "primary", Value: `
updated

`}},
}); err != nil {
t.Fatalf("Apply() error = %v", err)
@@ -70,7 +70,7 @@ func TestAcceptanceHTMLInlineReplaceHTML(t *testing.T) {
func TestAcceptanceAlternativeSetBodyUpdatesHTMLAndSummary(t *testing.T) {
snapshot := mustParseFixtureDraft(t, mustReadFixture(t, "testdata/alternative_draft.eml"))
- if err := Apply(snapshot, Patch{
+ if err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: "updated body
"}},
}); err != nil {
t.Fatalf("Apply() error = %v", err)
@@ -97,7 +97,7 @@ func TestAcceptanceCalendarDraftAppendPlainPreservesCalendar(t *testing.T) {
if originalCalendar == nil {
t.Fatalf("calendar part missing")
}
- if err := Apply(snapshot, Patch{
+ if err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "append_body", BodyKind: "text/plain", Selector: "primary", Value: "\nagenda"}},
}); err != nil {
t.Fatalf("Apply() error = %v", err)
@@ -122,7 +122,7 @@ func TestAcceptanceCalendarDraftAppendPlainPreservesCalendar(t *testing.T) {
func TestAcceptanceSignedDraftSubjectOnlyPreservesSignedEntity(t *testing.T) {
snapshot := mustParseFixtureDraft(t, mustReadFixture(t, "testdata/multipart_signed_draft.eml"))
originalBodyEntity := string(snapshot.Body.RawEntity)
- if err := Apply(snapshot, Patch{
+ if err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_subject", Value: "Signed updated"}},
}); err != nil {
t.Fatalf("Apply() error = %v", err)
@@ -144,7 +144,7 @@ func TestAcceptanceDirtyMultipartAppendPlainPreservesOuterNoise(t *testing.T) {
snapshot := mustParseFixtureDraft(t, mustReadFixture(t, "testdata/dirty_multipart_preamble.eml"))
originalPreamble := string(snapshot.Body.Preamble)
originalEpilogue := string(snapshot.Body.Epilogue)
- if err := Apply(snapshot, Patch{
+ if err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "append_body", BodyKind: "text/plain", Selector: "primary", Value: "\nworld"}},
}); err != nil {
t.Fatalf("Apply() error = %v", err)
diff --git a/shortcuts/mail/draft/model.go b/shortcuts/mail/draft/model.go
index 5c06da2f7..b772f9ce2 100644
--- a/shortcuts/mail/draft/model.go
+++ b/shortcuts/mail/draft/model.go
@@ -9,6 +9,8 @@ import (
"mime"
"net/mail"
"strings"
+
+ "github.com/larksuite/cli/extension/fileio"
)
type DraftRaw struct {
@@ -98,6 +100,12 @@ func (p *Part) FileName() string {
return ""
}
+// DraftCtx carries runtime dependencies for draft operations.
+// It is separate from DraftSnapshot to keep the snapshot a pure data model.
+type DraftCtx struct {
+ FIO fileio.FileIO
+}
+
type DraftSnapshot struct {
DraftID string
Headers []Header
diff --git a/shortcuts/mail/draft/patch.go b/shortcuts/mail/draft/patch.go
index 2b22edcd7..5ab6336e0 100644
--- a/shortcuts/mail/draft/patch.go
+++ b/shortcuts/mail/draft/patch.go
@@ -5,6 +5,7 @@ package draft
import (
"fmt"
+ "io"
"mime"
"path/filepath"
"regexp"
@@ -12,7 +13,6 @@ import (
"github.com/google/uuid"
"github.com/larksuite/cli/internal/validate"
- "github.com/larksuite/cli/internal/vfs"
"github.com/larksuite/cli/shortcuts/mail/filecheck"
)
@@ -39,26 +39,26 @@ var bodyChangingOps = map[string]bool{
"append_body": true,
}
-func Apply(snapshot *DraftSnapshot, patch Patch) error {
+func Apply(dctx *DraftCtx, snapshot *DraftSnapshot, patch Patch) error {
if err := patch.Validate(); err != nil {
return err
}
hasBodyChange := false
for _, op := range patch.Ops {
- if err := applyOp(snapshot, op, patch.Options); err != nil {
+ if err := applyOp(dctx, snapshot, op, patch.Options); err != nil {
return err
}
if bodyChangingOps[op.Op] {
hasBodyChange = true
}
}
- if err := postProcessInlineImages(snapshot, hasBodyChange); err != nil {
+ if err := postProcessInlineImages(dctx, snapshot, hasBodyChange); err != nil {
return err
}
return refreshSnapshot(snapshot)
}
-func applyOp(snapshot *DraftSnapshot, op PatchOp, options PatchOptions) error {
+func applyOp(dctx *DraftCtx, snapshot *DraftSnapshot, op PatchOp, options PatchOptions) error {
switch op.Op {
case "set_subject":
if strings.ContainsAny(op.Value, "\r\n") {
@@ -100,7 +100,7 @@ func applyOp(snapshot *DraftSnapshot, op PatchOp, options PatchOptions) error {
}
removeHeader(&snapshot.Headers, op.Name)
case "add_attachment":
- return addAttachment(snapshot, op.Path)
+ return addAttachment(dctx, snapshot, op.Path)
case "remove_attachment":
partID, err := resolveTarget(snapshot, op.Target)
if err != nil {
@@ -108,13 +108,13 @@ func applyOp(snapshot *DraftSnapshot, op PatchOp, options PatchOptions) error {
}
return removeAttachment(snapshot, partID)
case "add_inline":
- return addInline(snapshot, op.Path, op.CID, op.FileName, op.ContentType)
+ return addInline(dctx, snapshot, op.Path, op.CID, op.FileName, op.ContentType)
case "replace_inline":
partID, err := resolveTarget(snapshot, op.Target)
if err != nil {
return fmt.Errorf("replace_inline: %w", err)
}
- return replaceInline(snapshot, partID, op.Path, op.CID, op.FileName, op.ContentType)
+ return replaceInline(dctx, snapshot, partID, op.Path, op.CID, op.FileName, op.ContentType)
case "remove_inline":
partID, err := resolveTarget(snapshot, op.Target)
if err != nil {
@@ -478,22 +478,23 @@ func newMultipartContainer(mediaType string) *Part {
}
}
-func addAttachment(snapshot *DraftSnapshot, path string) error {
- safePath, err := validate.SafeInputPath(path)
- if err != nil {
- return fmt.Errorf("attachment %q: %w", path, err)
- }
+func addAttachment(dctx *DraftCtx, snapshot *DraftSnapshot, path string) error {
if err := checkBlockedExtension(filepath.Base(path)); err != nil {
return err
}
- info, err := vfs.Stat(safePath)
+ info, err := dctx.FIO.Stat(path)
if err != nil {
return err
}
if err := checkSnapshotAttachmentLimit(snapshot, info.Size(), nil); err != nil {
return err
}
- content, err := vfs.ReadFile(safePath)
+ f, err := dctx.FIO.Open(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ content, err := io.ReadAll(f)
if err != nil {
return err
}
@@ -543,19 +544,20 @@ func addAttachment(snapshot *DraftSnapshot, path string) error {
// creates a MIME inline part, and attaches it to the snapshot's
// multipart/related container. If container is non-nil it is reused;
// otherwise the container is resolved from the snapshot.
-func loadAndAttachInline(snapshot *DraftSnapshot, path, cid, fileName string, container *Part) (*Part, error) {
- safePath, err := validate.SafeInputPath(path)
- if err != nil {
- return nil, fmt.Errorf("inline image %q: %w", path, err)
- }
- info, err := vfs.Stat(safePath)
+func loadAndAttachInline(dctx *DraftCtx, snapshot *DraftSnapshot, path, cid, fileName string, container *Part) (*Part, error) {
+ info, err := dctx.FIO.Stat(path)
if err != nil {
return nil, fmt.Errorf("inline image %q: %w", path, err)
}
if err := checkSnapshotAttachmentLimit(snapshot, info.Size(), nil); err != nil {
return nil, err
}
- content, err := vfs.ReadFile(safePath)
+ f, err := dctx.FIO.Open(path)
+ if err != nil {
+ return nil, fmt.Errorf("inline image %q: %w", path, err)
+ }
+ defer f.Close()
+ content, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("inline image %q: %w", path, err)
}
@@ -567,7 +569,7 @@ func loadAndAttachInline(snapshot *DraftSnapshot, path, cid, fileName string, co
if err != nil {
return nil, fmt.Errorf("inline image %q: %w", path, err)
}
- inline, err := newInlinePart(safePath, content, cid, name, detectedCT)
+ inline, err := newInlinePart(path, content, cid, name, detectedCT)
if err != nil {
return nil, fmt.Errorf("inline image %q: %w", path, err)
}
@@ -586,12 +588,12 @@ func loadAndAttachInline(snapshot *DraftSnapshot, path, cid, fileName string, co
return container, nil
}
-func addInline(snapshot *DraftSnapshot, path, cid, fileName, contentType string) error {
- _, err := loadAndAttachInline(snapshot, path, cid, fileName, nil)
+func addInline(dctx *DraftCtx, snapshot *DraftSnapshot, path, cid, fileName, contentType string) error {
+ _, err := loadAndAttachInline(dctx, snapshot, path, cid, fileName, nil)
return err
}
-func replaceInline(snapshot *DraftSnapshot, partID, path, cid, fileName, contentType string) error {
+func replaceInline(dctx *DraftCtx, snapshot *DraftSnapshot, partID, path, cid, fileName, contentType string) error {
part := findPart(snapshot.Body, partID)
if part == nil {
return fmt.Errorf("inline part %q not found", partID)
@@ -599,18 +601,19 @@ func replaceInline(snapshot *DraftSnapshot, partID, path, cid, fileName, content
if !isInlinePart(part) {
return fmt.Errorf("part %q is not an inline MIME part", partID)
}
- safePath, err := validate.SafeInputPath(path)
- if err != nil {
- return fmt.Errorf("inline image %q: %w", path, err)
- }
- info, err := vfs.Stat(safePath)
+ info, err := dctx.FIO.Stat(path)
if err != nil {
return err
}
if err := checkSnapshotAttachmentLimit(snapshot, info.Size(), part); err != nil {
return err
}
- content, err := vfs.ReadFile(safePath)
+ f, err := dctx.FIO.Open(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ content, err := io.ReadAll(f)
if err != nil {
return err
}
@@ -990,7 +993,7 @@ func ResolveLocalImagePaths(html string) (string, []LocalImageRef, error) {
// resolveLocalImgSrc scans HTML for
references,
// creates MIME inline parts for each local file, and returns the HTML
// with those src attributes replaced by cid: URIs.
-func resolveLocalImgSrc(snapshot *DraftSnapshot, html string) (string, error) {
+func resolveLocalImgSrc(dctx *DraftCtx, snapshot *DraftSnapshot, html string) (string, error) {
resolved, refs, err := ResolveLocalImagePaths(html)
if err != nil {
return "", err
@@ -999,7 +1002,7 @@ func resolveLocalImgSrc(snapshot *DraftSnapshot, html string) (string, error) {
var container *Part
for _, ref := range refs {
fileName := filepath.Base(ref.FilePath)
- container, err = loadAndAttachInline(snapshot, ref.FilePath, ref.CID, fileName, container)
+ container, err = loadAndAttachInline(dctx, snapshot, ref.FilePath, ref.CID, fileName, container)
if err != nil {
return "", err
}
@@ -1092,7 +1095,7 @@ func FindOrphanedCIDs(html string, addedCIDs []string) []string {
// NOTE: The EML builder path has an equivalent function processInlineImagesForEML
// in shortcuts/mail/helpers.go. When adding new validation or processing logic here,
// update processInlineImagesForEML as well (or extract a shared function).
-func postProcessInlineImages(snapshot *DraftSnapshot, resolveLocal bool) error {
+func postProcessInlineImages(dctx *DraftCtx, snapshot *DraftSnapshot, resolveLocal bool) error {
htmlPart := findPrimaryBodyPart(snapshot.Body, "text/html")
if htmlPart == nil {
return nil
@@ -1102,7 +1105,7 @@ func postProcessInlineImages(snapshot *DraftSnapshot, resolveLocal bool) error {
html := origHTML
if resolveLocal {
var err error
- html, err = resolveLocalImgSrc(snapshot, origHTML)
+ html, err = resolveLocalImgSrc(dctx, snapshot, origHTML)
if err != nil {
return err
}
diff --git a/shortcuts/mail/draft/patch_attachment_test.go b/shortcuts/mail/draft/patch_attachment_test.go
index c7470199e..055a01dbe 100644
--- a/shortcuts/mail/draft/patch_attachment_test.go
+++ b/shortcuts/mail/draft/patch_attachment_test.go
@@ -25,9 +25,10 @@ func TestAddAttachmentToNilBodyCreatesRoot(t *testing.T) {
{Name: "From", Value: "alice@example.com"},
},
}
+ dctx := &DraftCtx{FIO: testFIO}
// Apply manually with a minimal patch (bypass Patch validation since we
// have no body part to detect)
- err := addAttachment(snapshot, "file.txt")
+ err := addAttachment(dctx, snapshot, "file.txt")
if err != nil {
t.Fatalf("addAttachment() error = %v", err)
}
@@ -51,7 +52,7 @@ func TestAddAttachmentToExistingMultipartMixed(t *testing.T) {
}
snapshot := mustParseFixtureDraft(t, fixtureData)
originalChildren := len(snapshot.Body.Children)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "add_attachment", Path: "second.txt"}},
})
if err != nil {
@@ -84,7 +85,7 @@ func TestAddAttachmentBlockedExtensionViaApply(t *testing.T) {
snapshot := mustParseFixtureDraft(t, fixtureData)
for _, name := range blocked {
t.Run(name, func(t *testing.T) {
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "add_attachment", Path: name}},
})
if err == nil {
@@ -111,7 +112,7 @@ func TestAddAttachmentAllowedExtensionViaApply(t *testing.T) {
for _, name := range allowed {
t.Run(name, func(t *testing.T) {
snapshot := mustParseFixtureDraft(t, fixtureData)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "add_attachment", Path: name}},
})
if err != nil {
@@ -142,7 +143,7 @@ Content-Type: text/html; charset=UTF-8
`)
for _, name := range []string{"icon.svg", "evil.png"} {
t.Run(name, func(t *testing.T) {
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "add_inline", Path: name, CID: "img1"}},
})
if err == nil {
@@ -167,7 +168,7 @@ Content-Type: text/html; charset=UTF-8
hello

`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "add_inline", Path: name, CID: "img1"}},
})
if err != nil {
@@ -194,7 +195,7 @@ Content-Type: text/html; charset=UTF-8
hello

`)
// User passes a spoofed content_type; it should be ignored in favor of detected type.
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "add_inline", Path: "logo.png", CID: "img1", ContentType: "application/octet-stream"}},
})
if err != nil {
@@ -234,7 +235,7 @@ PHN2Zz48L3N2Zz4=
// The old part has image/svg+xml. Replace with a PNG file; the filename
// falls back to the path ("new.png") since the old part's name is "icon.svg"
// which would fail the extension whitelist, so we pass an explicit filename.
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{
Op: "replace_inline",
Target: AttachmentTarget{PartID: "1.2"},
@@ -257,7 +258,7 @@ PHN2Zz48L3N2Zz4=
func TestRemoveAttachmentRejectsInlinePart(t *testing.T) {
snapshot := mustParseFixtureDraft(t, mustReadFixture(t, "testdata/html_inline_draft.eml"))
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "remove_attachment", Target: AttachmentTarget{PartID: "1.2"}}},
})
if err == nil || !strings.Contains(err.Error(), "use remove_inline") {
@@ -280,7 +281,7 @@ Content-Transfer-Encoding: base64
YQ==
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "remove_attachment", Target: AttachmentTarget{PartID: "1"}}},
})
if err == nil || !strings.Contains(err.Error(), "cannot remove root") {
@@ -301,7 +302,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "remove_attachment", Target: AttachmentTarget{PartID: "99"}}},
})
if err == nil || !strings.Contains(err.Error(), "not found") {
@@ -316,7 +317,7 @@ hello
func TestRemoveInlineRejectsNonInlinePart(t *testing.T) {
snapshot := mustParseFixtureDraft(t, mustReadFixture(t, "testdata/forward_draft.eml"))
// 1.2 is an attachment in forward_draft, not an inline
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "remove_inline", Target: AttachmentTarget{PartID: "1.2"}}},
})
if err == nil || !strings.Contains(err.Error(), "not an inline") {
@@ -340,7 +341,7 @@ Content-Transfer-Encoding: base64
cG5n
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "remove_inline", Target: AttachmentTarget{PartID: "1"}}},
})
if err == nil || !strings.Contains(err.Error(), "cannot remove root") {
@@ -361,7 +362,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "remove_inline", Target: AttachmentTarget{PartID: "99"}}},
})
if err == nil || !strings.Contains(err.Error(), "not found") {
@@ -376,7 +377,7 @@ hello
func TestResolveTargetByCID(t *testing.T) {
snapshot := mustParseFixtureDraft(t, mustReadFixture(t, "testdata/html_inline_draft.eml"))
// Remove via CID target
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{
Op: "replace_inline",
Target: AttachmentTarget{CID: "logo"},
@@ -397,7 +398,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "remove_inline", Target: AttachmentTarget{CID: "nonexistent"}}},
})
if err == nil || !strings.Contains(err.Error(), "no part with cid") {
@@ -433,7 +434,7 @@ func TestReplaceInlineRejectsNonInlinePart(t *testing.T) {
t.Fatal(err)
}
snapshot := mustParseFixtureDraft(t, fixtureData)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{
Op: "replace_inline",
Target: AttachmentTarget{PartID: "1.2"},
@@ -462,7 +463,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{
Op: "replace_inline",
Target: AttachmentTarget{PartID: "99"},
diff --git a/shortcuts/mail/draft/patch_body_test.go b/shortcuts/mail/draft/patch_body_test.go
index ff5718f76..46a081551 100644
--- a/shortcuts/mail/draft/patch_body_test.go
+++ b/shortcuts/mail/draft/patch_body_test.go
@@ -21,7 +21,7 @@ Content-Type: text/html; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: "updated
"}},
})
if err != nil {
@@ -43,7 +43,7 @@ Content-Type: text/html; charset=UTF-8
func TestApplySetBodyNoPrimaryBodyFails(t *testing.T) {
// A multipart/signed draft has no editable primary body
snapshot := mustParseFixtureDraft(t, mustReadFixture(t, "testdata/multipart_signed_draft.eml"))
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: "anything"}},
})
if err == nil || !strings.Contains(err.Error(), "no unique primary body") {
@@ -65,7 +65,7 @@ Content-Type: text/html; charset=UTF-8
old reply
`+quoteHTML+`
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_reply_body", Value: "new reply
"}},
})
if err != nil {
@@ -101,7 +101,7 @@ Content-Type: text/html; charset=UTF-8
old note
`+quoteHTML+`
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_reply_body", Value: "updated note
"}},
})
if err != nil {
@@ -130,7 +130,7 @@ Content-Type: text/html; charset=UTF-8
original body
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_reply_body", Value: "replaced
"}},
})
if err != nil {
@@ -164,7 +164,7 @@ Content-Type: text/html; charset=UTF-8
old reply
`+quoteHTML+`
--alt--
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_reply_body", Value: "new reply
"}},
})
if err != nil {
@@ -201,7 +201,7 @@ Content-Type: text/plain; charset=UTF-8
original text
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_reply_body", Value: "replaced text"}},
})
if err != nil {
@@ -226,7 +226,7 @@ Content-Type: text/plain; charset=UTF-8
original content
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "replace_body", BodyKind: "text/plain", Value: "replaced content"}},
})
if err != nil {
@@ -247,7 +247,7 @@ Content-Type: text/plain; charset=UTF-8
original
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "append_body", BodyKind: "text/plain", Value: " appended"}},
})
if err != nil {
@@ -272,7 +272,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "replace_body", BodyKind: "text/csv", Value: "data"}},
})
if err == nil || !strings.Contains(err.Error(), "body_kind must be text/plain or text/html") {
@@ -293,7 +293,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "replace_body", BodyKind: "text/html", Value: "new
"}},
})
if err == nil || !strings.Contains(err.Error(), "no primary text/html body part") {
@@ -322,7 +322,7 @@ Content-Type: text/html; charset=UTF-8
real body
--alt--
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: "just plain text without any tags"}},
})
if err == nil || !strings.Contains(err.Error(), "requires HTML input") {
@@ -343,7 +343,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{
{Op: "set_subject", Value: "Updated Subject"},
{Op: "add_recipient", Field: "cc", Name: "Carol", Address: "carol@example.com"},
diff --git a/shortcuts/mail/draft/patch_header_test.go b/shortcuts/mail/draft/patch_header_test.go
index 25ccf0132..e95775ad9 100644
--- a/shortcuts/mail/draft/patch_header_test.go
+++ b/shortcuts/mail/draft/patch_header_test.go
@@ -21,7 +21,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{
Op: "set_reply_to",
Addresses: []Address{{Name: "Support", Address: "support@example.com"}},
@@ -48,7 +48,7 @@ hello
if len(snapshot.ReplyTo) == 0 {
t.Fatalf("ReplyTo should be set before clear")
}
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "clear_reply_to"}},
})
if err != nil {
@@ -76,7 +76,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "remove_header", Name: "X-Priority"}},
})
if err != nil {
@@ -96,7 +96,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "remove_header", Name: "Content-Type"}},
})
if err == nil || !strings.Contains(err.Error(), "protected") {
@@ -114,7 +114,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "remove_header", Name: "Reply-To"}},
Options: PatchOptions{AllowProtectedHeaderEdits: true},
})
@@ -139,7 +139,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_header", Name: "Bad:Name", Value: "value"}},
})
if err == nil || !strings.Contains(err.Error(), "must not contain") {
@@ -156,7 +156,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_header", Name: "X-Custom", Value: "val\r\ninjected"}},
})
if err == nil || !strings.Contains(err.Error(), "must not contain") {
@@ -177,7 +177,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_subject", Value: "Subject\ninjection"}},
})
if err == nil || !strings.Contains(err.Error(), "must not contain") {
@@ -198,7 +198,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "unknown_op"}},
})
if err == nil || !strings.Contains(err.Error(), "unsupported") {
diff --git a/shortcuts/mail/draft/patch_inline_resolve_test.go b/shortcuts/mail/draft/patch_inline_resolve_test.go
index d45e0019d..04c7861a7 100644
--- a/shortcuts/mail/draft/patch_inline_resolve_test.go
+++ b/shortcuts/mail/draft/patch_inline_resolve_test.go
@@ -26,7 +26,7 @@ Content-Type: text/html; charset=UTF-8
Hello

`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: `Hello

`}},
})
if err != nil {
@@ -79,7 +79,7 @@ Content-Type: text/html; charset=UTF-8
empty
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: ``}},
})
if err != nil {
@@ -126,7 +126,7 @@ cG5n
htmlPart := findPart(snapshot.Body, snapshot.PrimaryHTMLPartID)
originalBody := string(htmlPart.Body)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: originalBody}},
})
if err != nil {
@@ -156,7 +156,7 @@ Content-Type: text/html; charset=UTF-8
empty
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: ``}},
})
if err != nil {
@@ -190,7 +190,7 @@ Content-Type: text/html; charset=UTF-8
empty
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: `
text

`}},
})
if err != nil {
@@ -235,7 +235,7 @@ Content-Type: text/html; charset=UTF-8
empty
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: ``}},
})
if err == nil {
@@ -268,7 +268,7 @@ cG5n
--rel--
`)
// Remove the
tag from body.
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: "hello
"}},
})
if err != nil {
@@ -309,7 +309,7 @@ cG5n
--rel--
`)
// Replace old image reference with a new local file.
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: ``}},
})
if err != nil {
@@ -352,7 +352,7 @@ Content-Type: text/html; charset=UTF-8
original reply
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_reply_body", Value: `new reply

`}},
})
if err != nil {
@@ -402,7 +402,7 @@ Content-Type: text/html; charset=UTF-8
empty
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{
{Op: "add_inline", Path: "a.png", CID: "a"},
{Op: "set_body", Value: ``},
@@ -449,7 +449,7 @@ Content-Type: text/html; charset=UTF-8
`)
// add_inline creates CID "logo", but body uses local path instead of cid:logo.
// resolve generates a UUID CID, orphan cleanup removes the unused "logo".
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{
{Op: "add_inline", Path: "logo.png", CID: "logo"},
{Op: "set_body", Value: ``},
@@ -503,7 +503,7 @@ cG5n
--rel--
`)
// remove_inline removes the MIME part, but set_body still references cid:logo.
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{
{Op: "remove_inline", Target: AttachmentTarget{CID: "logo"}},
{Op: "set_body", Value: ``},
@@ -541,7 +541,7 @@ Content-Transfer-Encoding: base64
cG5n
--rel--
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{
{Op: "remove_inline", Target: AttachmentTarget{CID: "old"}},
{Op: "set_body", Value: ``},
@@ -584,7 +584,7 @@ Content-Type: text/plain; charset=UTF-8
Just plain text.
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: "Updated plain text."}},
})
if err != nil {
@@ -631,7 +631,7 @@ cG5n
`)
// A metadata-only edit should not destroy the HTML body part even though
// its Content-ID is not referenced by any
.
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_subject", Value: "Updated subject"}},
})
if err != nil {
@@ -901,7 +901,7 @@ func TestSetBodyReplacesOrphanedInlineUnderMixed(t *testing.T) {
}
// Apply set_body with a local image path (triggers auto-resolve).
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: `111
222
`}},
})
if err != nil {
@@ -970,7 +970,7 @@ Content-Type: text/html; charset=UTF-8
Hello

`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_subject", Value: "Updated subject"}},
})
if err != nil {
diff --git a/shortcuts/mail/draft/patch_recipient_test.go b/shortcuts/mail/draft/patch_recipient_test.go
index 9aadfbb7d..52ea56dff 100644
--- a/shortcuts/mail/draft/patch_recipient_test.go
+++ b/shortcuts/mail/draft/patch_recipient_test.go
@@ -21,7 +21,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{
Op: "add_recipient",
Field: "to",
@@ -49,7 +49,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{
Op: "add_recipient",
Field: "to",
@@ -74,7 +74,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{
Op: "add_recipient",
Field: "cc",
@@ -99,7 +99,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{
Op: "add_recipient",
Field: "bcc",
@@ -124,7 +124,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{
Op: "add_recipient",
Field: "to",
@@ -150,7 +150,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{
Op: "remove_recipient",
Field: "to",
@@ -177,7 +177,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{
Op: "remove_recipient",
Field: "to",
@@ -201,7 +201,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{
Op: "remove_recipient",
Field: "to",
@@ -222,7 +222,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{
Op: "remove_recipient",
Field: "cc",
@@ -244,7 +244,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{
Op: "remove_recipient",
Field: "cc",
@@ -276,7 +276,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{
Op: "set_recipients",
Field: "cc",
diff --git a/shortcuts/mail/draft/patch_test.go b/shortcuts/mail/draft/patch_test.go
index f6543d137..57dd04025 100644
--- a/shortcuts/mail/draft/patch_test.go
+++ b/shortcuts/mail/draft/patch_test.go
@@ -7,8 +7,12 @@ import (
"os"
"strings"
"testing"
+
+ "github.com/larksuite/cli/internal/vfs/localfileio"
)
+var testFIO = &localfileio.LocalFileIO{}
+
func chdirTemp(t *testing.T) {
t.Helper()
orig, err := os.Getwd()
@@ -37,7 +41,7 @@ Content-Transfer-Encoding: 7bit
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_subject", Value: "Updated"}},
})
if err != nil {
@@ -67,7 +71,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_header", Name: "Message-ID", Value: ""}},
})
if err == nil {
@@ -84,7 +88,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{
Op: "set_recipients",
Field: "to",
@@ -115,7 +119,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: "updated"}},
})
if err != nil {
@@ -143,7 +147,7 @@ Content-Type: text/html; charset=UTF-8
hello
--alt--
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: ""}},
})
if err != nil {
@@ -174,7 +178,7 @@ Content-Type: text/html; charset=UTF-8
hello
--alt--
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: "updated plain text"}},
})
if err == nil || !strings.Contains(err.Error(), "draft main body is text/html") {
@@ -199,7 +203,7 @@ Content-Type: text/html; charset=UTF-8
hello world
--alt--
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: ""}},
})
if err != nil {
@@ -224,7 +228,7 @@ Content-Transfer-Encoding: 7bit
hello
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "replace_body", BodyKind: "text/html", Selector: "primary", Value: "hello
"}},
Options: PatchOptions{
RewriteEntireDraft: true,
@@ -264,7 +268,7 @@ Content-Type: text/html; charset=UTF-8
hello
--alt--
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "replace_body", BodyKind: "text/html", Selector: "primary", Value: "updated
"}},
})
if err == nil || !strings.Contains(err.Error(), "edit them together with set_body") {
@@ -289,7 +293,7 @@ Content-Type: text/html; charset=UTF-8
hello
--alt--
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "append_body", BodyKind: "text/plain", Selector: "primary", Value: "\nappend"}},
})
if err == nil || !strings.Contains(err.Error(), "edit them together with set_body") {
@@ -319,7 +323,7 @@ aGVsbG8=
--rel--
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "replace_body", BodyKind: "text/plain", Selector: "primary", Value: "hello plain"}},
Options: PatchOptions{
RewriteEntireDraft: true,
@@ -345,7 +349,7 @@ aGVsbG8=
func TestRemoveAttachmentKeepsRemainingOrder(t *testing.T) {
snapshot := mustParseFixtureDraft(t, mustReadFixture(t, "testdata/forward_draft.eml"))
- if err := Apply(snapshot, Patch{
+ if err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "remove_attachment", Target: AttachmentTarget{PartID: "1.3"}}},
}); err != nil {
t.Fatalf("Apply() error = %v", err)
@@ -380,7 +384,7 @@ Content-Transfer-Encoding: base64
cG5n
--rel--
`)
- if err := Apply(snapshot, Patch{
+ if err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "remove_inline", Target: AttachmentTarget{CID: "logo-cid"}}},
}); err != nil {
t.Fatalf("Apply() error = %v", err)
@@ -404,7 +408,7 @@ Content-Transfer-Encoding: 7bit
hello

`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{
{Op: "add_inline", Path: "logo.png", CID: "logo"},
},
@@ -431,7 +435,7 @@ func TestReplaceInlineKeepsCIDByDefault(t *testing.T) {
t.Fatalf("WriteFile() error = %v", err)
}
snapshot := mustParseFixtureDraft(t, fixtureData)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{
{Op: "replace_inline", Target: AttachmentTarget{PartID: "1.2"}, Path: "updated.png"},
},
@@ -450,7 +454,7 @@ func TestReplaceInlineKeepsCIDByDefault(t *testing.T) {
func TestRemoveInlineFailsWhenHTMLStillReferencesCID(t *testing.T) {
snapshot := mustParseFixtureDraft(t, mustReadFixture(t, "testdata/html_inline_draft.eml"))
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{
{Op: "remove_inline", Target: AttachmentTarget{PartID: "1.2"}},
},
@@ -481,7 +485,7 @@ cG5n
--rel--
`)
// set_body that drops the existing cid:logo reference → logo is auto-removed
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: "replaced body without cid reference
"}},
})
if err != nil {
@@ -516,7 +520,7 @@ cG5n
--rel--
`)
// set_body that preserves the existing cid:logo reference → should succeed
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: `updated body

`}},
})
if err != nil {
@@ -526,7 +530,7 @@ cG5n
func TestApplySetBodyRejectsSignedDraft(t *testing.T) {
snapshot := mustParseFixtureDraft(t, mustReadFixture(t, "testdata/multipart_signed_draft.eml"))
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: "updated"}},
})
if err == nil {
@@ -541,7 +545,7 @@ func TestApplyAppendTextKeepsCalendarPart(t *testing.T) {
t.Fatalf("calendar part missing before patch")
}
originalCalendar := string(calendar.RawEntity)
- if err := Apply(snapshot, Patch{
+ if err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "append_body", BodyKind: "text/plain", Selector: "primary", Value: "\nupdated"}},
}); err != nil {
t.Fatalf("Apply() error = %v", err)
@@ -565,7 +569,7 @@ Content-Type: text/plain; charset=UTF-8
hello
`)
- if err := Apply(snapshot, Patch{
+ if err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "add_attachment", Path: "note.txt"}},
}); err != nil {
t.Fatalf("Apply() error = %v", err)
@@ -603,7 +607,7 @@ Content-Type: text/html; charset=UTF-8
hello
`)
for _, bad := range []string{"my logo", "cid\there", "loid", "img(1)"} {
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "add_inline", Path: "logo.png", CID: bad}},
})
if err == nil {
@@ -631,7 +635,7 @@ Content-Type: text/html; charset=UTF-8
`)
// Step 1: add inline — this wraps body into multipart/related
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "add_inline", Path: "logo.png", CID: "logo"}},
})
if err != nil {
@@ -640,7 +644,7 @@ Content-Type: text/html; charset=UTF-8
// Step 2: set_body — this restructures the MIME tree, potentially making
// PrimaryHTMLPartID stale
- err = Apply(snapshot, Patch{
+ err = Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: `updated

`}},
})
if err != nil {
@@ -649,7 +653,7 @@ Content-Type: text/html; charset=UTF-8
// Step 3: set_body again dropping the CID reference — orphaned inline part
// should be auto-removed (not error), matching the auto-cleanup behavior.
- err = Apply(snapshot, Patch{
+ err = Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "set_body", Value: `no image here
`}},
})
if err != nil {
@@ -676,7 +680,7 @@ Content-Type: text/html; charset=UTF-8
hello
`)
for _, bad := range []string{"logo\ninjected", "logo\rinjected", "lo\r\ngo"} {
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "add_inline", Path: "logo.png", CID: bad}},
})
if err == nil {
@@ -699,7 +703,7 @@ Content-Type: text/html; charset=UTF-8
hello
`)
for _, bad := range []string{"logo\ninjected.png", "logo\r.png", "lo\r\ngo.png"} {
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "add_inline", Path: "logo.png", CID: "safecid", FileName: bad}},
})
if err == nil {
@@ -716,7 +720,7 @@ func TestReplaceInlineRejectsInvalidCharactersInCID(t *testing.T) {
}
snapshot := mustParseFixtureDraft(t, fixtureData)
for _, bad := range []string{"my logo", "cid\there", "loid", "img(1)"} {
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "replace_inline", Target: AttachmentTarget{PartID: "1.2"}, Path: "updated.png", CID: bad}},
})
if err == nil {
@@ -735,7 +739,7 @@ func TestReplaceInlineRejectsCRLFInCID(t *testing.T) {
}
snapshot := mustParseFixtureDraft(t, fixtureData)
for _, bad := range []string{"logo\ninjected", "logo\rinjected"} {
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "replace_inline", Target: AttachmentTarget{PartID: "1.2"}, Path: "updated.png", CID: bad}},
})
if err == nil {
@@ -752,7 +756,7 @@ func TestReplaceInlineRejectsInvalidCIDChars(t *testing.T) {
}
snapshot := mustParseFixtureDraft(t, fixtureData)
for _, bad := range []string{"my logo", "a\tb", "cid", "cid(x)"} {
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "replace_inline", Target: AttachmentTarget{PartID: "1.2"}, Path: "updated.png", CID: bad}},
})
if err == nil {
@@ -769,7 +773,7 @@ func TestReplaceInlineRejectsCRLFInFileName(t *testing.T) {
}
snapshot := mustParseFixtureDraft(t, fixtureData)
for _, bad := range []string{"logo\ninjected.png", "logo\r.png"} {
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "replace_inline", Target: AttachmentTarget{PartID: "1.2"}, Path: "updated.png", FileName: bad}},
})
if err == nil {
diff --git a/shortcuts/mail/draft/serialize_golden_test.go b/shortcuts/mail/draft/serialize_golden_test.go
index 83cf8be2b..3fd8a1d21 100644
--- a/shortcuts/mail/draft/serialize_golden_test.go
+++ b/shortcuts/mail/draft/serialize_golden_test.go
@@ -81,7 +81,7 @@ func TestSerializeGoldenFixtures(t *testing.T) {
if tc.patchFn != nil {
patch = tc.patchFn(t)
}
- if err := Apply(snapshot, patch); err != nil {
+ if err := Apply(&DraftCtx{FIO: testFIO}, snapshot, patch); err != nil {
t.Fatalf("Apply() error = %v", err)
}
raw, err := Serialize(snapshot)
diff --git a/shortcuts/mail/draft/serialize_test.go b/shortcuts/mail/draft/serialize_test.go
index 834b84cd2..72d1fec30 100644
--- a/shortcuts/mail/draft/serialize_test.go
+++ b/shortcuts/mail/draft/serialize_test.go
@@ -41,7 +41,7 @@ aGVsbG8=
--mix--
`)
- err := Apply(snapshot, Patch{
+ err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{
{Op: "set_subject", Value: "Updated"},
{Op: "set_body", Value: "updated body
"},
@@ -104,7 +104,7 @@ aGVsbG8=
--mix--
`
snapshot := mustParseFixtureDraft(t, original)
- if err := Apply(snapshot, Patch{Ops: []PatchOp{{Op: "set_subject", Value: "Updated"}}}); err != nil {
+ if err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{Ops: []PatchOp{{Op: "set_subject", Value: "Updated"}}}); err != nil {
t.Fatalf("Apply() error = %v", err)
}
serialized, err := Serialize(snapshot)
@@ -141,7 +141,7 @@ Content-Transfer-Encoding: quoted-printable
caf=E9
`)
- if err := Apply(snapshot, Patch{
+ if err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "append_body", BodyKind: "text/plain", Selector: "primary", Value: " déjà"}},
}); err != nil {
t.Fatalf("Apply() error = %v", err)
@@ -173,7 +173,7 @@ caf=E9
func TestSerializeSubjectOnlyPreservesEmbeddedMessageAttachment(t *testing.T) {
original := mustReadFixture(t, "testdata/message_rfc822_draft.eml")
snapshot := mustParseFixtureDraft(t, original)
- if err := Apply(snapshot, Patch{Ops: []PatchOp{{Op: "set_subject", Value: "Updated forward"}}}); err != nil {
+ if err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{Ops: []PatchOp{{Op: "set_subject", Value: "Updated forward"}}}); err != nil {
t.Fatalf("Apply() error = %v", err)
}
serialized, err := Serialize(snapshot)
@@ -196,7 +196,7 @@ func TestSerializeSubjectOnlyPreservesEmbeddedMessageAttachment(t *testing.T) {
func TestSerializeSubjectOnlyPreservesSignedBodyEntity(t *testing.T) {
original := mustReadFixture(t, "testdata/multipart_signed_draft.eml")
snapshot := mustParseFixtureDraft(t, original)
- if err := Apply(snapshot, Patch{Ops: []PatchOp{{Op: "set_subject", Value: "Updated signed"}}}); err != nil {
+ if err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{Ops: []PatchOp{{Op: "set_subject", Value: "Updated signed"}}}); err != nil {
t.Fatalf("Apply() error = %v", err)
}
serialized, err := Serialize(snapshot)
@@ -226,7 +226,7 @@ func TestSerializeSubjectOnlyPreservesSignedBodyEntity(t *testing.T) {
func TestSerializeDirtyMultipartKeepsPreambleAndEpilogue(t *testing.T) {
original := mustReadFixture(t, "testdata/dirty_multipart_preamble.eml")
snapshot := mustParseFixtureDraft(t, original)
- if err := Apply(snapshot, Patch{
+ if err := Apply(&DraftCtx{FIO: testFIO}, snapshot, Patch{
Ops: []PatchOp{{Op: "append_body", BodyKind: "text/plain", Selector: "primary", Value: "\nworld"}},
}); err != nil {
t.Fatalf("Apply() error = %v", err)
diff --git a/shortcuts/mail/emlbuilder/builder.go b/shortcuts/mail/emlbuilder/builder.go
index dd0ca9559..8aa027cb5 100644
--- a/shortcuts/mail/emlbuilder/builder.go
+++ b/shortcuts/mail/emlbuilder/builder.go
@@ -44,6 +44,7 @@ import (
"bytes"
"encoding/base64"
"fmt"
+ "io"
"math/rand"
"mime"
"net/mail"
@@ -51,27 +52,28 @@ import (
"strings"
"time"
- "github.com/larksuite/cli/internal/validate"
- "github.com/larksuite/cli/internal/vfs"
+ "github.com/larksuite/cli/extension/fileio"
"github.com/larksuite/cli/shortcuts/mail/filecheck"
)
// MaxEMLSize is the maximum allowed raw EML size in bytes.
const MaxEMLSize = 25 * 1024 * 1024 // 25 MB
-// readFile reads the named file and returns its contents.
-func readFile(path string) ([]byte, error) {
- safePath, err := validate.SafeInputPath(path)
+// readFile reads the named file and returns its contents via FileIO.
+func readFile(fio fileio.FileIO, path string) ([]byte, error) {
+ f, err := fio.Open(path)
if err != nil {
return nil, fmt.Errorf("attachment %q: %w", path, err)
}
- return vfs.ReadFile(safePath)
+ defer f.Close()
+ return io.ReadAll(f)
}
// Builder constructs a Lark-compatible RFC 2822 EML message.
// All setter methods return a copy of the Builder (immutable/fluent style),
// so a base builder can be reused across multiple goroutines safely.
type Builder struct {
+ fio fileio.FileIO // injected via WithFileIO; must be set before AddFile* calls
from mail.Address
to []mail.Address
cc []mail.Address
@@ -93,6 +95,12 @@ type Builder struct {
err error
}
+// WithFileIO returns a copy of b with the given FileIO.
+func (b Builder) WithFileIO(fio fileio.FileIO) Builder {
+ b.fio = fio
+ return b
+}
+
type attachment struct {
content []byte
contentType string
@@ -425,7 +433,7 @@ func (b Builder) AddFileAttachment(path string) Builder {
b.err = err
return b
}
- content, err := readFile(path)
+ content, err := readFile(b.fio, path)
if err != nil {
b.err = err
return b
@@ -480,7 +488,7 @@ func (b Builder) AddFileInline(path, contentID string) Builder {
if b.err != nil {
return b
}
- content, err := readFile(path)
+ content, err := readFile(b.fio, path)
if err != nil {
b.err = err
return b
@@ -539,7 +547,7 @@ func (b Builder) AddFileOtherPart(path, contentID string) Builder {
if b.err != nil {
return b
}
- content, err := readFile(path)
+ content, err := readFile(b.fio, path)
if err != nil {
b.err = err
return b
diff --git a/shortcuts/mail/emlbuilder/builder_test.go b/shortcuts/mail/emlbuilder/builder_test.go
index cef114751..37e989ccd 100644
--- a/shortcuts/mail/emlbuilder/builder_test.go
+++ b/shortcuts/mail/emlbuilder/builder_test.go
@@ -10,9 +10,14 @@ import (
"strings"
"testing"
"time"
+
+ "github.com/larksuite/cli/internal/vfs/localfileio"
)
-var fixedDate = time.Date(2026, 3, 20, 12, 0, 0, 0, time.UTC)
+var (
+ fixedDate = time.Date(2026, 3, 20, 12, 0, 0, 0, time.UTC)
+ testFIO = &localfileio.LocalFileIO{}
+)
// parseEML splits an EML string into a header block and body.
func splitHeaderBody(eml string) (headers, body string) {
@@ -960,7 +965,7 @@ func TestAddFileAttachmentBlockedExtension(t *testing.T) {
}
for _, name := range blocked {
t.Run(name, func(t *testing.T) {
- _, err := New().
+ _, err := New().WithFileIO(testFIO).
From("", "alice@example.com").
To("", "bob@example.com").
Subject("test").
@@ -993,7 +998,7 @@ func TestAddFileInlineBlockedFormat(t *testing.T) {
for _, name := range []string{"icon.svg", "evil.png"} {
t.Run(name, func(t *testing.T) {
- _, err := New().
+ _, err := New().WithFileIO(testFIO).
From("", "alice@example.com").
To("", "bob@example.com").
Subject("test").
@@ -1022,7 +1027,7 @@ func TestAddFileInlineAllowedFormat(t *testing.T) {
for _, name := range []string{"logo.png", "photo.jpg"} {
t.Run(name, func(t *testing.T) {
- _, err := New().
+ _, err := New().WithFileIO(testFIO).
From("", "alice@example.com").
To("", "bob@example.com").
Subject("test").
@@ -1050,7 +1055,7 @@ func TestAddFileAttachmentAllowedExtension(t *testing.T) {
}
for _, name := range allowed {
t.Run(name, func(t *testing.T) {
- _, err := New().
+ _, err := New().WithFileIO(testFIO).
From("", "alice@example.com").
To("", "bob@example.com").
Subject("test").
diff --git a/shortcuts/mail/helpers.go b/shortcuts/mail/helpers.go
index 005cf9792..4483681f6 100644
--- a/shortcuts/mail/helpers.go
+++ b/shortcuts/mail/helpers.go
@@ -6,6 +6,7 @@ package mail
import (
"encoding/base64"
"encoding/json"
+ "errors"
"fmt"
"io"
"mime"
@@ -17,10 +18,10 @@ import (
"strconv"
"strings"
+ "github.com/larksuite/cli/extension/fileio"
"github.com/larksuite/cli/internal/auth"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/validate"
- "github.com/larksuite/cli/internal/vfs"
"github.com/larksuite/cli/shortcuts/common"
draftpkg "github.com/larksuite/cli/shortcuts/mail/draft"
"github.com/larksuite/cli/shortcuts/mail/emlbuilder"
@@ -1858,7 +1859,7 @@ func inlineSpecFilePaths(specs []InlineSpec) []string {
// MaxAttachmentCount or the combined size exceeds MaxAttachmentBytes.
// filePaths are read via os.Stat (no full read); extraBytes / extraCount account for
// already-loaded content (e.g. downloaded original attachments in +forward).
-func checkAttachmentSizeLimit(filePaths []string, extraBytes int64, extraCount ...int) error {
+func checkAttachmentSizeLimit(fio fileio.FileIO, filePaths []string, extraBytes int64, extraCount ...int) error {
extra := 0
for _, c := range extraCount {
extra += c
@@ -1869,12 +1870,11 @@ func checkAttachmentSizeLimit(filePaths []string, extraBytes int64, extraCount .
}
totalBytes := extraBytes
for _, p := range filePaths {
- safePath, err := validate.SafeInputPath(p)
- if err != nil {
- return fmt.Errorf("unsafe attachment path %s: %w", p, err)
- }
- info, err := vfs.Stat(safePath)
+ info, err := fio.Stat(p)
if err != nil {
+ if errors.Is(err, fileio.ErrPathValidation) {
+ return fmt.Errorf("unsafe attachment path %s: %w", p, err)
+ }
return fmt.Errorf("failed to stat attachment %s: %w", p, err)
}
totalBytes += info.Size()
@@ -1976,7 +1976,7 @@ func validateRecipientCount(to, cc, bcc string) error {
return nil
}
-func validateComposeInlineAndAttachments(attachFlag, inlineFlag string, plainText bool, body string) error {
+func validateComposeInlineAndAttachments(fio fileio.FileIO, attachFlag, inlineFlag string, plainText bool, body string) error {
if strings.TrimSpace(inlineFlag) != "" {
if plainText {
return fmt.Errorf("--inline is not supported with --plain-text (inline images require HTML body)")
@@ -1994,5 +1994,5 @@ func validateComposeInlineAndAttachments(attachFlag, inlineFlag string, plainTex
return err
}
allFiles := append(splitByComma(attachFlag), inlineSpecFilePaths(inlineSpecs)...)
- return checkAttachmentSizeLimit(allFiles, 0)
+ return checkAttachmentSizeLimit(fio, allFiles, 0)
}
diff --git a/shortcuts/mail/helpers_test.go b/shortcuts/mail/helpers_test.go
index 13ab7f143..478bc8577 100644
--- a/shortcuts/mail/helpers_test.go
+++ b/shortcuts/mail/helpers_test.go
@@ -19,6 +19,7 @@ import (
"github.com/spf13/cobra"
"github.com/larksuite/cli/internal/cmdutil"
+ "github.com/larksuite/cli/internal/vfs/localfileio"
"github.com/larksuite/cli/shortcuts/common"
"github.com/larksuite/cli/shortcuts/mail/emlbuilder"
)
@@ -568,13 +569,13 @@ func TestToOriginalMessageForCompose_EmptyReferences(t *testing.T) {
// ---------------------------------------------------------------------------
func TestCheckAttachmentSizeLimit_NoFiles(t *testing.T) {
- if err := checkAttachmentSizeLimit(nil, 0); err != nil {
+ if err := checkAttachmentSizeLimit(nil, nil, 0); err != nil { //nolint:staticcheck // fio nil ok: no files
t.Fatalf("unexpected error for empty: %v", err)
}
}
func TestCheckAttachmentSizeLimit_CountExceeded(t *testing.T) {
- err := checkAttachmentSizeLimit(nil, 0, MaxAttachmentCount+1)
+ err := checkAttachmentSizeLimit(nil, nil, 0, MaxAttachmentCount+1)
if err == nil {
t.Fatal("expected error for count exceeded")
}
@@ -585,7 +586,7 @@ func TestCheckAttachmentSizeLimit_CountExceeded(t *testing.T) {
func TestCheckAttachmentSizeLimit_SizeExceeded(t *testing.T) {
// extraBytes alone exceeds the limit
- err := checkAttachmentSizeLimit(nil, MaxAttachmentBytes+1)
+ err := checkAttachmentSizeLimit(nil, nil, MaxAttachmentBytes+1)
if err == nil {
t.Fatal("expected error for size exceeded")
}
@@ -608,7 +609,8 @@ func TestCheckAttachmentSizeLimit_WithFiles(t *testing.T) {
}
defer os.Chdir(oldWd)
- err := checkAttachmentSizeLimit([]string{"./small.txt"}, 0)
+ fio := &localfileio.LocalFileIO{}
+ err := checkAttachmentSizeLimit(fio, []string{"./small.txt"}, 0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
diff --git a/shortcuts/mail/mail_draft_create.go b/shortcuts/mail/mail_draft_create.go
index c0dd63734..60c4dab5d 100644
--- a/shortcuts/mail/mail_draft_create.go
+++ b/shortcuts/mail/mail_draft_create.go
@@ -71,7 +71,7 @@ var MailDraftCreate = common.Shortcut{
if strings.TrimSpace(runtime.Str("body")) == "" {
return output.ErrValidation("--body is required; pass the full email body")
}
- if err := validateComposeInlineAndAttachments(runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), runtime.Str("body")); err != nil {
+ if err := validateComposeInlineAndAttachments(runtime.FileIO(), runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), runtime.Str("body")); err != nil {
return err
}
return nil
@@ -133,7 +133,7 @@ func buildRawEMLForDraftCreate(runtime *common.RuntimeContext, input draftCreate
return "", err
}
- bld := emlbuilder.New().
+ bld := emlbuilder.New().WithFileIO(runtime.FileIO()).
AllowNoRecipients().
Subject(input.Subject)
if strings.TrimSpace(input.To) != "" {
@@ -178,7 +178,7 @@ func buildRawEMLForDraftCreate(runtime *common.RuntimeContext, input draftCreate
bld = bld.TextBody([]byte(input.Body))
}
allFilePaths := append(append(splitByComma(input.Attach), inlineSpecFilePaths(inlineSpecs)...), autoResolvedPaths...)
- if err := checkAttachmentSizeLimit(allFilePaths, 0); err != nil {
+ if err := checkAttachmentSizeLimit(runtime.FileIO(), allFilePaths, 0); err != nil {
return "", err
}
for _, path := range splitByComma(input.Attach) {
diff --git a/shortcuts/mail/mail_draft_edit.go b/shortcuts/mail/mail_draft_edit.go
index e699c0edf..0113e94b0 100644
--- a/shortcuts/mail/mail_draft_edit.go
+++ b/shortcuts/mail/mail_draft_edit.go
@@ -11,8 +11,6 @@ import (
"strings"
"github.com/larksuite/cli/internal/output"
- "github.com/larksuite/cli/internal/validate"
- "github.com/larksuite/cli/internal/vfs"
"github.com/larksuite/cli/shortcuts/common"
draftpkg "github.com/larksuite/cli/shortcuts/mail/draft"
)
@@ -93,7 +91,8 @@ var MailDraftEdit = common.Shortcut{
if err != nil {
return output.ErrValidation("parse draft raw EML failed: %v", err)
}
- if err := draftpkg.Apply(snapshot, patch); err != nil {
+ dctx := &draftpkg.DraftCtx{FIO: runtime.FileIO()}
+ if err := draftpkg.Apply(dctx, snapshot, patch); err != nil {
return output.ErrValidation("apply draft patch failed: %v", err)
}
serialized, err := draftpkg.Serialize(snapshot)
@@ -216,7 +215,7 @@ func buildDraftEditPatch(runtime *common.RuntimeContext) (draftpkg.Patch, error)
patchFile := strings.TrimSpace(runtime.Str("patch-file"))
if patchFile != "" {
- filePatch, err := loadPatchFile(patchFile)
+ filePatch, err := loadPatchFile(runtime, patchFile)
if err != nil {
return patch, err
}
@@ -264,13 +263,14 @@ func buildDraftEditPatch(runtime *common.RuntimeContext) (draftpkg.Patch, error)
return patch, patch.Validate()
}
-func loadPatchFile(path string) (draftpkg.Patch, error) {
+func loadPatchFile(runtime *common.RuntimeContext, path string) (draftpkg.Patch, error) {
var patch draftpkg.Patch
- safePath, err := validate.SafeInputPath(path)
+ f, err := runtime.FileIO().Open(path)
if err != nil {
return patch, fmt.Errorf("--patch-file %q: %w", path, err)
}
- data, err := vfs.ReadFile(safePath)
+ defer f.Close()
+ data, err := io.ReadAll(f)
if err != nil {
return patch, err
}
diff --git a/shortcuts/mail/mail_forward.go b/shortcuts/mail/mail_forward.go
index 0d3f57463..290fdf614 100644
--- a/shortcuts/mail/mail_forward.go
+++ b/shortcuts/mail/mail_forward.go
@@ -63,7 +63,7 @@ var MailForward = common.Shortcut{
return err
}
}
- return validateComposeInlineAndAttachments(runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), "")
+ return validateComposeInlineAndAttachments(runtime.FileIO(), runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), "")
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
messageId := runtime.Str("message-id")
@@ -99,7 +99,7 @@ var MailForward = common.Shortcut{
return err
}
- bld := emlbuilder.New().
+ bld := emlbuilder.New().WithFileIO(runtime.FileIO()).
Subject(buildForwardSubject(orig.subject)).
ToAddrs(parseNetAddrs(to))
if senderEmail != "" {
@@ -195,7 +195,7 @@ var MailForward = common.Shortcut{
bld = bld.Header("X-Lms-Large-Attachment-Ids", base64.StdEncoding.EncodeToString(idsJSON))
}
allFilePaths := append(append(splitByComma(attachFlag), inlineSpecFilePaths(inlineSpecs)...), autoResolvedPaths...)
- if err := checkAttachmentSizeLimit(allFilePaths, origAttBytes, len(origAtts)); err != nil {
+ if err := checkAttachmentSizeLimit(runtime.FileIO(), allFilePaths, origAttBytes, len(origAtts)); err != nil {
return err
}
for _, att := range origAtts {
diff --git a/shortcuts/mail/mail_reply.go b/shortcuts/mail/mail_reply.go
index e4e64511d..1f125e61b 100644
--- a/shortcuts/mail/mail_reply.go
+++ b/shortcuts/mail/mail_reply.go
@@ -55,7 +55,7 @@ var MailReply = common.Shortcut{
if err := validateConfirmSendScope(runtime); err != nil {
return err
}
- return validateComposeInlineAndAttachments(runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), "")
+ return validateComposeInlineAndAttachments(runtime.FileIO(), runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), "")
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
messageId := runtime.Str("message-id")
@@ -110,7 +110,7 @@ var MailReply = common.Shortcut{
}
quoted := quoteForReply(&orig, useHTML)
- bld := emlbuilder.New().
+ bld := emlbuilder.New().WithFileIO(runtime.FileIO()).
Subject(buildReplySubject(orig.subject)).
ToAddrs(parseNetAddrs(replyTo))
if senderEmail != "" {
@@ -161,7 +161,7 @@ var MailReply = common.Shortcut{
bld = bld.TextBody([]byte(bodyStr + quoted))
}
allFilePaths := append(append(splitByComma(attachFlag), inlineSpecFilePaths(inlineSpecs)...), autoResolvedPaths...)
- if err := checkAttachmentSizeLimit(allFilePaths, 0); err != nil {
+ if err := checkAttachmentSizeLimit(runtime.FileIO(), allFilePaths, 0); err != nil {
return err
}
for _, path := range splitByComma(attachFlag) {
diff --git a/shortcuts/mail/mail_reply_all.go b/shortcuts/mail/mail_reply_all.go
index 8a5e01247..7a5640869 100644
--- a/shortcuts/mail/mail_reply_all.go
+++ b/shortcuts/mail/mail_reply_all.go
@@ -56,7 +56,7 @@ var MailReplyAll = common.Shortcut{
if err := validateConfirmSendScope(runtime); err != nil {
return err
}
- return validateComposeInlineAndAttachments(runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), "")
+ return validateComposeInlineAndAttachments(runtime.FileIO(), runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), "")
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
messageId := runtime.Str("message-id")
@@ -124,7 +124,7 @@ var MailReplyAll = common.Shortcut{
bodyStr = body
}
quoted := quoteForReply(&orig, useHTML)
- bld := emlbuilder.New().
+ bld := emlbuilder.New().WithFileIO(runtime.FileIO()).
Subject(buildReplySubject(orig.subject)).
ToAddrs(parseNetAddrs(toList))
if senderEmail != "" {
@@ -175,7 +175,7 @@ var MailReplyAll = common.Shortcut{
bld = bld.TextBody([]byte(bodyStr + quoted))
}
allFilePaths := append(append(splitByComma(attachFlag), inlineSpecFilePaths(inlineSpecs)...), autoResolvedPaths...)
- if err := checkAttachmentSizeLimit(allFilePaths, 0); err != nil {
+ if err := checkAttachmentSizeLimit(runtime.FileIO(), allFilePaths, 0); err != nil {
return err
}
for _, path := range splitByComma(attachFlag) {
diff --git a/shortcuts/mail/mail_send.go b/shortcuts/mail/mail_send.go
index 533915b21..789586803 100644
--- a/shortcuts/mail/mail_send.go
+++ b/shortcuts/mail/mail_send.go
@@ -61,7 +61,7 @@ var MailSend = common.Shortcut{
if err := validateComposeHasAtLeastOneRecipient(runtime.Str("to"), runtime.Str("cc"), runtime.Str("bcc")); err != nil {
return err
}
- return validateComposeInlineAndAttachments(runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), runtime.Str("body"))
+ return validateComposeInlineAndAttachments(runtime.FileIO(), runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), runtime.Str("body"))
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
to := runtime.Str("to")
@@ -80,7 +80,7 @@ var MailSend = common.Shortcut{
senderEmail = fetchCurrentUserEmail(runtime)
}
- bld := emlbuilder.New().
+ bld := emlbuilder.New().WithFileIO(runtime.FileIO()).
Subject(subject).
ToAddrs(parseNetAddrs(to))
if senderEmail != "" {
@@ -122,7 +122,7 @@ var MailSend = common.Shortcut{
bld = bld.TextBody([]byte(body))
}
allFilePaths := append(append(splitByComma(attachFlag), inlineSpecFilePaths(inlineSpecs)...), autoResolvedPaths...)
- if err := checkAttachmentSizeLimit(allFilePaths, 0); err != nil {
+ if err := checkAttachmentSizeLimit(runtime.FileIO(), allFilePaths, 0); err != nil {
return err
}