bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
113 lines
4.8 KiB
Go
113 lines
4.8 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package mail
|
|
|
|
import (
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/larksuite/cli/extension/fileio"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
// bodyFileFlag is the shared `--body-file` flag declaration reused by every
|
|
// compose shortcut (+send / +draft-create / +reply / +reply-all / +forward).
|
|
// All six shortcuts honour the same mutual-exclusion contract with `--body`
|
|
// and the cwd-subtree path safety rule. The flag is intentionally NOT
|
|
// shared with `+lint-html` because that command's description differs
|
|
// ("HTML to lint" vs "email body") in a way that is more readable when
|
|
// authored per-shortcut. `+draft-edit` does not expose `--body-file` either
|
|
// — its body ops flow through `--patch-file` JSON whose `value` field is
|
|
// the natural file-based entry point for large bodies.
|
|
var bodyFileFlag = common.Flag{
|
|
Name: "body-file",
|
|
Desc: "Path (relative, within cwd subtree) to a file containing the email body HTML. Mutually exclusive with --body. Size capped at 32 MB.",
|
|
Input: []string{common.File},
|
|
}
|
|
|
|
// maxBodyFileSize caps the size of a `--body-file` HTML input. The compose
|
|
// path's downstream EML limit is 25 MB (helpers.go MAX_EML_BYTES); we allow a
|
|
// bit more headroom here (32 MB) so a body close to the limit still loads
|
|
// before the downstream check fires with a clearer error message. The cap
|
|
// prevents an `io.ReadAll` from blowing memory on a misdirected gigabyte
|
|
// file.
|
|
const maxBodyFileSize = 32 * 1024 * 1024 // 32 MB
|
|
|
|
// validateBodyFileMutex enforces the `--body` / `--body-file` mutual
|
|
// exclusion + cwd-subtree path safety. Compose shortcuts call this in
|
|
// their Validate phase so AI / users see a clear error before any work
|
|
// runs. Pass the shortcut's RuntimeContext-resolved flag values directly:
|
|
// `bodyFlag` is the `--body` value (may be empty), `bodyFile` is the
|
|
// trimmed `--body-file` value, and `validatePath` is the
|
|
// runtime.ValidatePath bound function used to enforce the relative-path
|
|
// rule (cwd-subtree only; no absolute / `..` traversal).
|
|
//
|
|
// Returns an ErrValidation error when either invariant is violated, nil
|
|
// otherwise. The "exactly one of {--body, --body-file}" check is
|
|
// shortcut-specific (some shortcuts allow neither, e.g. `+forward` with
|
|
// no explicit body) and is therefore left to the caller.
|
|
func validateBodyFileMutex(bodyFlag, bodyFile string, validatePath func(string) error) error {
|
|
bodyEmpty := strings.TrimSpace(bodyFlag) == ""
|
|
if !bodyEmpty && bodyFile != "" {
|
|
return mailValidationError("--body and --body-file are mutually exclusive; pass exactly one").
|
|
WithParams(
|
|
mailInvalidParam("--body", "mutually exclusive with --body-file"),
|
|
mailInvalidParam("--body-file", "mutually exclusive with --body"),
|
|
)
|
|
}
|
|
if bodyFile != "" {
|
|
if err := validatePath(bodyFile); err != nil {
|
|
return mailValidationParamError("--body-file", "--body-file: %v", err).WithCause(err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// resolveBodyFromFlags returns the body content from --body or --body-file.
|
|
// Validate has already enforced mutual exclusion via validateBodyFileMutex,
|
|
// so exactly one is set (or neither when a template / parent message
|
|
// supplies the body). Returns ("", nil) when neither flag is set so
|
|
// downstream code can decide whether the empty body is allowed.
|
|
func resolveBodyFromFlags(runtime *common.RuntimeContext) (string, error) {
|
|
if body := runtime.Str("body"); strings.TrimSpace(body) != "" {
|
|
return body, nil
|
|
}
|
|
path := strings.TrimSpace(runtime.Str("body-file"))
|
|
if path == "" {
|
|
return "", nil
|
|
}
|
|
return readBodyFile(runtime.FileIO(), path)
|
|
}
|
|
|
|
func validateRequiredResolvedBody(body string, hasTemplate bool, message string) error {
|
|
if !hasTemplate && strings.TrimSpace(body) == "" {
|
|
return mailValidationError("%s", message)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// readBodyFile loads --body-file content with a size cap. Returns an
|
|
// ErrValidation error if the file exceeds maxBodyFileSize or any IO error
|
|
// occurs. The size check uses io.LimitReader(maxBodyFileSize+1) so any
|
|
// over-cap byte is observable without reading the whole file.
|
|
//
|
|
// Callers MUST have run runtime.ValidatePath(path) on `path` first — the
|
|
// helper only opens the file via the supplied FileIO and does not repeat
|
|
// the cwd-subtree safety check.
|
|
func readBodyFile(fio fileio.FileIO, path string) (string, error) {
|
|
f, err := fio.Open(path)
|
|
if err != nil {
|
|
return "", mailValidationParamError("--body-file", "open --body-file %s: %v", path, err).WithCause(mailInputStatError(err))
|
|
}
|
|
defer f.Close()
|
|
buf, err := io.ReadAll(io.LimitReader(f, maxBodyFileSize+1))
|
|
if err != nil {
|
|
return "", mailValidationParamError("--body-file", "read --body-file %s: %v", path, err).WithCause(err)
|
|
}
|
|
if len(buf) > maxBodyFileSize {
|
|
return "", mailValidationParamError("--body-file", "--body-file: file exceeds %d MB limit", maxBodyFileSize/1024/1024)
|
|
}
|
|
return string(buf), nil
|
|
}
|