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
quoted text
`) - 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: "
updated body
"}}, }) 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: "
updated body
"}}, }) 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 }