From f8b08f2da8d5b80024b6f136ce76f82cf8d0667b Mon Sep 17 00:00:00 2001
From: tuxedomm <273098272+tuxedomm@users.noreply.github.com>
Date: Wed, 8 Apr 2026 18:58:04 +0800
Subject: [PATCH 1/5] refactor: migrate mail shortcuts to FileIO
- DraftSnapshot.FIO: inject FileIO into draft snapshot for patch ops
(addAttachment, loadAndAttachInline, replaceInline)
- emlbuilder.Builder.fio: inject via WithFileIO(), readFile uses FileIO.Open
- mail_draft_edit: loadPatchFile uses runtime.FileIO().Open
- helpers: checkAttachmentSizeLimit takes fio param, uses FileIO.Stat
- validateComposeInlineAndAttachments: pass fio through to size check
- All mail entry points (send/reply/reply_all/forward/draft_create):
pass runtime.FileIO() to builder and size limit checks
Change-Id: I580b126a970c57b4ccfcf13dab4d6aacbe255de9
---
shortcuts/mail/draft/model.go | 3 ++
shortcuts/mail/draft/patch.go | 43 ++++++++++---------
shortcuts/mail/draft/patch_attachment_test.go | 1 +
shortcuts/mail/draft/patch_test.go | 5 +++
shortcuts/mail/emlbuilder/builder.go | 26 +++++++----
shortcuts/mail/emlbuilder/builder_test.go | 15 ++++---
shortcuts/mail/helpers.go | 14 +++---
shortcuts/mail/helpers_test.go | 10 +++--
shortcuts/mail/mail_draft_create.go | 6 +--
shortcuts/mail/mail_draft_edit.go | 12 +++---
shortcuts/mail/mail_forward.go | 6 +--
shortcuts/mail/mail_reply.go | 6 +--
shortcuts/mail/mail_reply_all.go | 6 +--
shortcuts/mail/mail_send.go | 6 +--
14 files changed, 91 insertions(+), 68 deletions(-)
diff --git a/shortcuts/mail/draft/model.go b/shortcuts/mail/draft/model.go
index 5c06da2f7..48a3261cf 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 {
@@ -99,6 +101,7 @@ func (p *Part) FileName() string {
}
type DraftSnapshot struct {
+ FIO fileio.FileIO `json:"-"` // injected file I/O; nil falls back to legacy validate+vfs
DraftID string
Headers []Header
Body *Part
diff --git a/shortcuts/mail/draft/patch.go b/shortcuts/mail/draft/patch.go
index 2b22edcd7..bf2792093 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"
)
@@ -479,21 +479,22 @@ 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)
- }
if err := checkBlockedExtension(filepath.Base(path)); err != nil {
return err
}
- info, err := vfs.Stat(safePath)
+ info, err := snapshot.FIO.Stat(path)
if err != nil {
- return err
+ return fmt.Errorf("attachment %q: %w", path, err)
}
if err := checkSnapshotAttachmentLimit(snapshot, info.Size(), nil); err != nil {
return err
}
- content, err := vfs.ReadFile(safePath)
+ f, err := snapshot.FIO.Open(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ content, err := io.ReadAll(f)
if err != nil {
return err
}
@@ -544,18 +545,19 @@ func addAttachment(snapshot *DraftSnapshot, path string) error {
// 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)
+ info, err := snapshot.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 := snapshot.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)
}
@@ -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)
+ info, err := snapshot.FIO.Stat(path)
if err != nil {
return fmt.Errorf("inline image %q: %w", path, err)
}
- info, err := vfs.Stat(safePath)
- if err != nil {
+ if err := checkSnapshotAttachmentLimit(snapshot, info.Size(), part); err != nil {
return err
}
- if err := checkSnapshotAttachmentLimit(snapshot, info.Size(), part); err != nil {
+ f, err := snapshot.FIO.Open(path)
+ if err != nil {
return err
}
- content, err := vfs.ReadFile(safePath)
+ defer f.Close()
+ content, err := io.ReadAll(f)
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..1538dfd11 100644
--- a/shortcuts/mail/draft/patch_attachment_test.go
+++ b/shortcuts/mail/draft/patch_attachment_test.go
@@ -19,6 +19,7 @@ func TestAddAttachmentToNilBodyCreatesRoot(t *testing.T) {
t.Fatal(err)
}
snapshot := &DraftSnapshot{
+ FIO: testFIO,
DraftID: "d-nil",
Headers: []Header{
{Name: "Subject", Value: "Empty"},
diff --git a/shortcuts/mail/draft/patch_test.go b/shortcuts/mail/draft/patch_test.go
index f6543d137..97444a107 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()
@@ -784,6 +788,7 @@ func mustParseFixtureDraft(t *testing.T, raw string) *DraftSnapshot {
if err != nil {
t.Fatalf("Parse() error = %v", err)
}
+ snapshot.FIO = testFIO
return snapshot
}
diff --git a/shortcuts/mail/emlbuilder/builder.go b/shortcuts/mail/emlbuilder/builder.go
index dd0ca9559..19f893a4e 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 file I/O; nil falls back to legacy validate+vfs
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..ed8c1f3b3 100644
--- a/shortcuts/mail/helpers.go
+++ b/shortcuts/mail/helpers.go
@@ -17,10 +17,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 +1858,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,14 +1869,10 @@ func checkAttachmentSizeLimit(filePaths []string, extraBytes int64, extraCount .
}
totalBytes := extraBytes
for _, p := range filePaths {
- safePath, err := validate.SafeInputPath(p)
+ info, err := fio.Stat(p)
if err != nil {
return fmt.Errorf("unsafe attachment path %s: %w", p, err)
}
- info, err := vfs.Stat(safePath)
- if err != nil {
- return fmt.Errorf("failed to stat attachment %s: %w", p, err)
- }
totalBytes += info.Size()
}
if totalBytes > MaxAttachmentBytes {
@@ -1976,7 +1972,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 +1990,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..ee9b1d0fc 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,6 +91,7 @@ var MailDraftEdit = common.Shortcut{
if err != nil {
return output.ErrValidation("parse draft raw EML failed: %v", err)
}
+ snapshot.FIO = runtime.FileIO()
if err := draftpkg.Apply(snapshot, patch); err != nil {
return output.ErrValidation("apply draft patch failed: %v", err)
}
@@ -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
}
From e2158b241bf93df4c78de346598597545421ccc3 Mon Sep 17 00:00:00 2001
From: tuxedomm <273098272+tuxedomm@users.noreply.github.com>
Date: Wed, 8 Apr 2026 19:18:59 +0800
Subject: [PATCH 2/5] fix: preserve original error messages in mail draft patch
and helpers
- addAttachment/replaceInline: distinguish path validation errors
(wrapped with context) from stat failures (bare error), matching
original SafeInputPath + vfs.Stat two-step behavior
- checkAttachmentSizeLimit: distinguish "unsafe attachment path" (path
validation) from "failed to stat attachment" (file not found/perm)
Change-Id: I6e632c949aa9803a05ef63f529c3db9dfa8ab839
---
shortcuts/mail/draft/patch.go | 12 ++++++++++--
shortcuts/mail/helpers.go | 6 +++++-
2 files changed, 15 insertions(+), 3 deletions(-)
diff --git a/shortcuts/mail/draft/patch.go b/shortcuts/mail/draft/patch.go
index bf2792093..adfd50cb8 100644
--- a/shortcuts/mail/draft/patch.go
+++ b/shortcuts/mail/draft/patch.go
@@ -4,6 +4,7 @@
package draft
import (
+ "errors"
"fmt"
"io"
"mime"
@@ -12,6 +13,7 @@ import (
"strings"
"github.com/google/uuid"
+ "github.com/larksuite/cli/extension/fileio"
"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/shortcuts/mail/filecheck"
)
@@ -484,7 +486,10 @@ func addAttachment(snapshot *DraftSnapshot, path string) error {
}
info, err := snapshot.FIO.Stat(path)
if err != nil {
- return fmt.Errorf("attachment %q: %w", path, err)
+ if errors.Is(err, fileio.ErrPathValidation) {
+ return fmt.Errorf("attachment %q: %w", path, err)
+ }
+ return err
}
if err := checkSnapshotAttachmentLimit(snapshot, info.Size(), nil); err != nil {
return err
@@ -603,7 +608,10 @@ func replaceInline(snapshot *DraftSnapshot, partID, path, cid, fileName, content
}
info, err := snapshot.FIO.Stat(path)
if err != nil {
- return fmt.Errorf("inline image %q: %w", path, err)
+ if errors.Is(err, fileio.ErrPathValidation) {
+ return fmt.Errorf("inline image %q: %w", path, err)
+ }
+ return err
}
if err := checkSnapshotAttachmentLimit(snapshot, info.Size(), part); err != nil {
return err
diff --git a/shortcuts/mail/helpers.go b/shortcuts/mail/helpers.go
index ed8c1f3b3..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"
@@ -1871,7 +1872,10 @@ func checkAttachmentSizeLimit(fio fileio.FileIO, filePaths []string, extraBytes
for _, p := range filePaths {
info, err := fio.Stat(p)
if err != nil {
- return fmt.Errorf("unsafe attachment path %s: %w", p, err)
+ 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()
}
From 078566320f671aac0d36b050507d9bc44663723e Mon Sep 17 00:00:00 2001
From: tuxedomm <273098272+tuxedomm@users.noreply.github.com>
Date: Wed, 8 Apr 2026 21:59:29 +0800
Subject: [PATCH 3/5] fix: restore validation order and fix misleading comments
in mail migration
- addAttachment: restore original order (path validation via Stat before
extension check) so path traversal errors are reported before blocked
extension errors
- DraftSnapshot.FIO comment: remove "nil falls back" claim; nil panics
- Builder.fio comment: same fix
Change-Id: I21ff209987c72704f2302ed3481c58e2a4256764
---
shortcuts/mail/draft/model.go | 2 +-
shortcuts/mail/draft/patch.go | 6 +++---
shortcuts/mail/emlbuilder/builder.go | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/shortcuts/mail/draft/model.go b/shortcuts/mail/draft/model.go
index 48a3261cf..14935a3a6 100644
--- a/shortcuts/mail/draft/model.go
+++ b/shortcuts/mail/draft/model.go
@@ -101,7 +101,7 @@ func (p *Part) FileName() string {
}
type DraftSnapshot struct {
- FIO fileio.FileIO `json:"-"` // injected file I/O; nil falls back to legacy validate+vfs
+ FIO fileio.FileIO `json:"-"` // injected file I/O; must be set before calling Apply
DraftID string
Headers []Header
Body *Part
diff --git a/shortcuts/mail/draft/patch.go b/shortcuts/mail/draft/patch.go
index adfd50cb8..ca7b9598f 100644
--- a/shortcuts/mail/draft/patch.go
+++ b/shortcuts/mail/draft/patch.go
@@ -481,9 +481,6 @@ func newMultipartContainer(mediaType string) *Part {
}
func addAttachment(snapshot *DraftSnapshot, path string) error {
- if err := checkBlockedExtension(filepath.Base(path)); err != nil {
- return err
- }
info, err := snapshot.FIO.Stat(path)
if err != nil {
if errors.Is(err, fileio.ErrPathValidation) {
@@ -491,6 +488,9 @@ func addAttachment(snapshot *DraftSnapshot, path string) error {
}
return err
}
+ if err := checkBlockedExtension(filepath.Base(path)); err != nil {
+ return err
+ }
if err := checkSnapshotAttachmentLimit(snapshot, info.Size(), nil); err != nil {
return err
}
diff --git a/shortcuts/mail/emlbuilder/builder.go b/shortcuts/mail/emlbuilder/builder.go
index 19f893a4e..8aa027cb5 100644
--- a/shortcuts/mail/emlbuilder/builder.go
+++ b/shortcuts/mail/emlbuilder/builder.go
@@ -73,7 +73,7 @@ func readFile(fio fileio.FileIO, path string) ([]byte, error) {
// 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 file I/O; nil falls back to legacy validate+vfs
+ fio fileio.FileIO // injected via WithFileIO; must be set before AddFile* calls
from mail.Address
to []mail.Address
cc []mail.Address
From ed1c3ee2e9e2a23c198383b993b706f86f305080 Mon Sep 17 00:00:00 2001
From: tuxedomm <273098272+tuxedomm@users.noreply.github.com>
Date: Thu, 9 Apr 2026 12:53:58 +0800
Subject: [PATCH 4/5] refactor: extract DraftCtx from DraftSnapshot for runtime
dependencies
Move fileio.FileIO out of DraftSnapshot (pure data model) into a
separate DraftCtx struct, keeping data and runtime concerns decoupled.
Apply and internal file-operation functions now receive *DraftCtx as
an explicit parameter.
Change-Id: Ibabb77c389f75db8cc92d3558a350774e90d1ce1
---
shortcuts/mail/draft/acceptance_test.go | 12 ++--
shortcuts/mail/draft/model.go | 7 ++-
shortcuts/mail/draft/patch.go | 44 ++++++-------
shortcuts/mail/draft/patch_attachment_test.go | 38 +++++------
shortcuts/mail/draft/patch_body_test.go | 26 ++++----
shortcuts/mail/draft/patch_header_test.go | 18 +++---
.../mail/draft/patch_inline_resolve_test.go | 34 +++++-----
shortcuts/mail/draft/patch_recipient_test.go | 22 +++----
shortcuts/mail/draft/patch_test.go | 63 +++++++++----------
shortcuts/mail/draft/serialize_golden_test.go | 2 +-
shortcuts/mail/draft/serialize_test.go | 12 ++--
shortcuts/mail/mail_draft_edit.go | 4 +-
12 files changed, 143 insertions(+), 139 deletions(-)
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 14935a3a6..b772f9ce2 100644
--- a/shortcuts/mail/draft/model.go
+++ b/shortcuts/mail/draft/model.go
@@ -100,8 +100,13 @@ 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 {
- FIO fileio.FileIO `json:"-"` // injected file I/O; must be set before calling Apply
DraftID string
Headers []Header
Body *Part
diff --git a/shortcuts/mail/draft/patch.go b/shortcuts/mail/draft/patch.go
index ca7b9598f..42d5fca01 100644
--- a/shortcuts/mail/draft/patch.go
+++ b/shortcuts/mail/draft/patch.go
@@ -41,26 +41,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") {
@@ -102,7 +102,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 {
@@ -110,13 +110,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 {
@@ -480,8 +480,8 @@ func newMultipartContainer(mediaType string) *Part {
}
}
-func addAttachment(snapshot *DraftSnapshot, path string) error {
- info, err := snapshot.FIO.Stat(path)
+func addAttachment(dctx *DraftCtx, snapshot *DraftSnapshot, path string) error {
+ info, err := dctx.FIO.Stat(path)
if err != nil {
if errors.Is(err, fileio.ErrPathValidation) {
return fmt.Errorf("attachment %q: %w", path, err)
@@ -494,7 +494,7 @@ func addAttachment(snapshot *DraftSnapshot, path string) error {
if err := checkSnapshotAttachmentLimit(snapshot, info.Size(), nil); err != nil {
return err
}
- f, err := snapshot.FIO.Open(path)
+ f, err := dctx.FIO.Open(path)
if err != nil {
return err
}
@@ -549,15 +549,15 @@ 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) {
- info, err := snapshot.FIO.Stat(path)
+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
}
- f, err := snapshot.FIO.Open(path)
+ f, err := dctx.FIO.Open(path)
if err != nil {
return nil, fmt.Errorf("inline image %q: %w", path, err)
}
@@ -593,12 +593,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)
@@ -606,7 +606,7 @@ func replaceInline(snapshot *DraftSnapshot, partID, path, cid, fileName, content
if !isInlinePart(part) {
return fmt.Errorf("part %q is not an inline MIME part", partID)
}
- info, err := snapshot.FIO.Stat(path)
+ info, err := dctx.FIO.Stat(path)
if err != nil {
if errors.Is(err, fileio.ErrPathValidation) {
return fmt.Errorf("inline image %q: %w", path, err)
@@ -616,7 +616,7 @@ func replaceInline(snapshot *DraftSnapshot, partID, path, cid, fileName, content
if err := checkSnapshotAttachmentLimit(snapshot, info.Size(), part); err != nil {
return err
}
- f, err := snapshot.FIO.Open(path)
+ f, err := dctx.FIO.Open(path)
if err != nil {
return err
}
@@ -1001,7 +1001,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
@@ -1010,7 +1010,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
}
@@ -1103,7 +1103,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
@@ -1113,7 +1113,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 1538dfd11..055a01dbe 100644
--- a/shortcuts/mail/draft/patch_attachment_test.go
+++ b/shortcuts/mail/draft/patch_attachment_test.go
@@ -19,16 +19,16 @@ func TestAddAttachmentToNilBodyCreatesRoot(t *testing.T) {
t.Fatal(err)
}
snapshot := &DraftSnapshot{
- FIO: testFIO,
DraftID: "d-nil",
Headers: []Header{
{Name: "Subject", Value: "Empty"},
{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)
}
@@ -52,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 {
@@ -85,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 {
@@ -112,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 {
@@ -143,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 {
@@ -168,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 {
@@ -195,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 {
@@ -235,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"},
@@ -258,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") {
@@ -281,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") {
@@ -302,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") {
@@ -317,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") {
@@ -341,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") {
@@ -362,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") {
@@ -377,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"},
@@ -398,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") {
@@ -434,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"},
@@ -463,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 97444a107..57dd04025 100644
--- a/shortcuts/mail/draft/patch_test.go
+++ b/shortcuts/mail/draft/patch_test.go
@@ -41,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 {
@@ -71,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 {
@@ -88,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",
@@ -119,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 {
@@ -147,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 {
@@ -178,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") {
@@ -203,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 {
@@ -228,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,
@@ -268,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") {
@@ -293,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") {
@@ -323,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,
@@ -349,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)
@@ -384,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)
@@ -408,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"},
},
@@ -435,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"},
},
@@ -454,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"}},
},
@@ -485,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 {
@@ -520,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 {
@@ -530,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 {
@@ -545,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)
@@ -569,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)
@@ -607,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 {
@@ -635,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 {
@@ -644,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 {
@@ -653,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 {
@@ -680,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 {
@@ -703,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 {
@@ -720,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 {
@@ -739,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 {
@@ -756,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 {
@@ -773,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 {
@@ -788,7 +788,6 @@ func mustParseFixtureDraft(t *testing.T, raw string) *DraftSnapshot {
if err != nil {
t.Fatalf("Parse() error = %v", err)
}
- snapshot.FIO = testFIO
return snapshot
}
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/mail_draft_edit.go b/shortcuts/mail/mail_draft_edit.go
index ee9b1d0fc..0113e94b0 100644
--- a/shortcuts/mail/mail_draft_edit.go
+++ b/shortcuts/mail/mail_draft_edit.go
@@ -91,8 +91,8 @@ var MailDraftEdit = common.Shortcut{
if err != nil {
return output.ErrValidation("parse draft raw EML failed: %v", err)
}
- snapshot.FIO = runtime.FileIO()
- 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)
From 1c3be9873808789e3b9feb6f92dcb28f253d0f9e Mon Sep 17 00:00:00 2001
From: tuxedomm <273098272+tuxedomm@users.noreply.github.com>
Date: Thu, 9 Apr 2026 13:10:56 +0800
Subject: [PATCH 5/5] fix: restore original validation order in addAttachment
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Move checkBlockedExtension back before FIO.Stat to match the original
SafeInputPath → checkBlockedExtension → Stat order. Also remove unused
errors and fileio imports.
Change-Id: I42e726be30409d03a16bb7306625732fd103d8b9
---
shortcuts/mail/draft/patch.go | 14 +++-----------
1 file changed, 3 insertions(+), 11 deletions(-)
diff --git a/shortcuts/mail/draft/patch.go b/shortcuts/mail/draft/patch.go
index 42d5fca01..5ab6336e0 100644
--- a/shortcuts/mail/draft/patch.go
+++ b/shortcuts/mail/draft/patch.go
@@ -4,7 +4,6 @@
package draft
import (
- "errors"
"fmt"
"io"
"mime"
@@ -13,7 +12,6 @@ import (
"strings"
"github.com/google/uuid"
- "github.com/larksuite/cli/extension/fileio"
"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/shortcuts/mail/filecheck"
)
@@ -481,14 +479,11 @@ func newMultipartContainer(mediaType string) *Part {
}
func addAttachment(dctx *DraftCtx, snapshot *DraftSnapshot, path string) error {
- info, err := dctx.FIO.Stat(path)
- if err != nil {
- if errors.Is(err, fileio.ErrPathValidation) {
- return fmt.Errorf("attachment %q: %w", path, err)
- }
+ if err := checkBlockedExtension(filepath.Base(path)); err != nil {
return err
}
- if err := checkBlockedExtension(filepath.Base(path)); err != nil {
+ info, err := dctx.FIO.Stat(path)
+ if err != nil {
return err
}
if err := checkSnapshotAttachmentLimit(snapshot, info.Size(), nil); err != nil {
@@ -608,9 +603,6 @@ func replaceInline(dctx *DraftCtx, snapshot *DraftSnapshot, partID, path, cid, f
}
info, err := dctx.FIO.Stat(path)
if err != nil {
- if errors.Is(err, fileio.ErrPathValidation) {
- return fmt.Errorf("inline image %q: %w", path, err)
- }
return err
}
if err := checkSnapshotAttachmentLimit(snapshot, info.Size(), part); err != nil {