mirror of
https://github.com/larksuite/cli.git
synced 2026-08-03 08:32:46 +08:00
feat(mail): expose draft priority in --inspect projection and document --set-priority (#779)
Add a Priority field to DraftProjection populated from the EML header pair X-Cli-Priority (CLI/OAPI primary) → X-Priority (RFC fallback for IMAP-回灌 historical drafts), with case-insensitive lookup via the existing headerValue helper and a local mapping table aligned with the backend gopkg/mail_priority.PriorityValueToType vocabulary. When neither header is present (the symmetric read of --set-priority normal=remove_header) the projection emits "unknown" so agents have a stable read-side surface. Append one notes entry to buildDraftEditPatchTemplate documenting the --set-priority flag and the X-Cli-Priority translation contract. The write-side (--set-priority flag, parsePriority helper, translation branch in mail_draft_edit.go, EML header target) is unchanged — already shipped on master. sprint: S4
This commit is contained in:
@@ -166,6 +166,7 @@ type DraftProjection struct {
|
||||
LargeAttachmentsSummary []LargeAttachmentSummary `json:"large_attachments_summary,omitempty"`
|
||||
InlineSummary []PartSummary `json:"inline_summary,omitempty"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
Priority string `json:"priority"`
|
||||
}
|
||||
|
||||
type Patch struct {
|
||||
|
||||
@@ -140,9 +140,53 @@ func Project(snapshot *DraftSnapshot) DraftProjection {
|
||||
|
||||
proj.LargeAttachmentsSummary = projectLargeAttachments(snapshot.Headers, htmlBody)
|
||||
|
||||
proj.Priority = parsePriorityFromHeaders(snapshot.Headers)
|
||||
|
||||
return proj
|
||||
}
|
||||
|
||||
// parsePriorityFromHeaders derives the read-side priority projection from
|
||||
// EML headers. It mirrors the write-side helper helpers.go:parsePriority
|
||||
// (which translates --set-priority high|normal|low into set_header /
|
||||
// remove_header X-Cli-Priority ops). Lookup order is case-insensitive
|
||||
// via headerValue:
|
||||
// 1. X-Cli-Priority (CLI/OAPI-specific header recognised by
|
||||
// mail-data-access headersToPbBodyExtra)
|
||||
// 2. X-Priority (RFC standard, fallback for IMAP-回灌 historical drafts)
|
||||
//
|
||||
// When neither header is present (including after the write-side translates
|
||||
// --set-priority normal into remove_header X-Cli-Priority), this returns
|
||||
// "normal" — absence of a priority header is the standard email convention
|
||||
// for normal priority. Agents cannot distinguish "explicitly normal" from
|
||||
// "never set" — known limitation.
|
||||
func parsePriorityFromHeaders(headers []Header) string {
|
||||
if v := headerValue(headers, "X-Cli-Priority"); v != "" {
|
||||
return mapPriorityValue(v)
|
||||
}
|
||||
if v := headerValue(headers, "X-Priority"); v != "" {
|
||||
return mapPriorityValue(v)
|
||||
}
|
||||
return "normal"
|
||||
}
|
||||
|
||||
// mapPriorityValue normalises a raw priority header value to the projection
|
||||
// vocabulary {"high","normal","low","unknown"}. The accepted input table is
|
||||
// kept in sync with backend gopkg/mail_priority.PriorityValueToType so that
|
||||
// CLI read-side projection observes the same set of values the server
|
||||
// recognises on write.
|
||||
func mapPriorityValue(raw string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||
case "1", "high", "1 (highest)":
|
||||
return "high"
|
||||
case "3", "normal", "3 (normal)":
|
||||
return "normal"
|
||||
case "5", "low", "5 (lowest)":
|
||||
return "low"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// projectLargeAttachments extracts large attachment info from the draft.
|
||||
// It first tries the server-format header (X-Lark-Large-Attachment) which
|
||||
// carries filename and size directly. Falls back to merging CLI-format
|
||||
|
||||
@@ -178,6 +178,170 @@ func TestSplitAtQuoteFalsePositivePlainText(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Priority projection (X-Cli-Priority primary, X-Priority fallback)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestProjectPriorityXCliPriorityHigh(t *testing.T) {
|
||||
snapshot := mustParseFixtureDraft(t, `Subject: priority high
|
||||
From: Alice <alice@example.com>
|
||||
To: Bob <bob@example.com>
|
||||
X-Cli-Priority: 1
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
hello
|
||||
`)
|
||||
proj := Project(snapshot)
|
||||
if proj.Priority != "high" {
|
||||
t.Fatalf("Priority = %q, want %q", proj.Priority, "high")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectPriorityFallbackXPriorityLow(t *testing.T) {
|
||||
// Only the standard X-Priority header is present (e.g. an IMAP-回灌
|
||||
// historical draft). The fallback path should kick in.
|
||||
snapshot := mustParseFixtureDraft(t, `Subject: priority low (fallback)
|
||||
From: Alice <alice@example.com>
|
||||
To: Bob <bob@example.com>
|
||||
X-Priority: 5
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
hello
|
||||
`)
|
||||
proj := Project(snapshot)
|
||||
if proj.Priority != "low" {
|
||||
t.Fatalf("Priority = %q, want %q", proj.Priority, "low")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectPriorityBothAbsentNormal(t *testing.T) {
|
||||
// Neither header is present — default priority is normal.
|
||||
snapshot := mustParseFixtureDraft(t, `Subject: no priority
|
||||
From: Alice <alice@example.com>
|
||||
To: Bob <bob@example.com>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
hello
|
||||
`)
|
||||
proj := Project(snapshot)
|
||||
if proj.Priority != "normal" {
|
||||
t.Fatalf("Priority = %q, want %q", proj.Priority, "normal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectPriorityXCliPriorityOutlookStyleHigh(t *testing.T) {
|
||||
// X-Cli-Priority set to the Outlook-style string "high" (any case).
|
||||
snapshot := mustParseFixtureDraft(t, `Subject: priority high (string)
|
||||
From: Alice <alice@example.com>
|
||||
To: Bob <bob@example.com>
|
||||
X-Cli-Priority: High
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
hello
|
||||
`)
|
||||
proj := Project(snapshot)
|
||||
if proj.Priority != "high" {
|
||||
t.Fatalf("Priority = %q, want %q", proj.Priority, "high")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectPriorityUnmappedValueUnknown(t *testing.T) {
|
||||
// Value outside the recognised mapping table (e.g. "urgent") falls
|
||||
// back to "unknown".
|
||||
snapshot := mustParseFixtureDraft(t, `Subject: priority urgent
|
||||
From: Alice <alice@example.com>
|
||||
To: Bob <bob@example.com>
|
||||
X-Cli-Priority: urgent
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
hello
|
||||
`)
|
||||
proj := Project(snapshot)
|
||||
if proj.Priority != "unknown" {
|
||||
t.Fatalf("Priority = %q, want %q", proj.Priority, "unknown")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectPriorityXCliPriorityWinsOverXPriority(t *testing.T) {
|
||||
// X-Cli-Priority must take precedence over X-Priority when both are
|
||||
// set (defensive: agent or upstream may write both).
|
||||
snapshot := mustParseFixtureDraft(t, `Subject: both headers
|
||||
From: Alice <alice@example.com>
|
||||
To: Bob <bob@example.com>
|
||||
X-Cli-Priority: 1
|
||||
X-Priority: 5
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
hello
|
||||
`)
|
||||
proj := Project(snapshot)
|
||||
if proj.Priority != "high" {
|
||||
t.Fatalf("Priority = %q, want %q (X-Cli-Priority must win)", proj.Priority, "high")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectPriorityNormalThree(t *testing.T) {
|
||||
// X-Cli-Priority=3 → "normal" (rare in CLI write path since
|
||||
// `--set-priority normal` actually removes the header, but this case
|
||||
// covers e.g. a draft set by another OAPI client that wrote 3).
|
||||
snapshot := mustParseFixtureDraft(t, `Subject: priority three
|
||||
From: Alice <alice@example.com>
|
||||
To: Bob <bob@example.com>
|
||||
X-Cli-Priority: 3
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
hello
|
||||
`)
|
||||
proj := Project(snapshot)
|
||||
if proj.Priority != "normal" {
|
||||
t.Fatalf("Priority = %q, want %q", proj.Priority, "normal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectPriorityFallbackXPriorityNormalString(t *testing.T) {
|
||||
// IMAP-回灌 / external client writes the RFC-standard `X-Priority: Normal`
|
||||
// string. The fallback path must project this as "normal" — symmetric with
|
||||
// how `X-Priority: High` / `Low` are already handled.
|
||||
snapshot := mustParseFixtureDraft(t, `Subject: priority normal (fallback)
|
||||
From: Alice <alice@example.com>
|
||||
To: Bob <bob@example.com>
|
||||
X-Priority: Normal
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
hello
|
||||
`)
|
||||
proj := Project(snapshot)
|
||||
if proj.Priority != "normal" {
|
||||
t.Fatalf("Priority = %q, want %q", proj.Priority, "normal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectPriorityOutlookStyleThreeNormal(t *testing.T) {
|
||||
// Outlook-style `3 (Normal)` parenthesised form — symmetric with the
|
||||
// already-supported `1 (Highest)` / `5 (Lowest)`.
|
||||
snapshot := mustParseFixtureDraft(t, `Subject: priority three (normal)
|
||||
From: Alice <alice@example.com>
|
||||
To: Bob <bob@example.com>
|
||||
X-Priority: 3 (Normal)
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
hello
|
||||
`)
|
||||
proj := Project(snapshot)
|
||||
if proj.Priority != "normal" {
|
||||
t.Fatalf("Priority = %q, want %q", proj.Priority, "normal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMissingInlineCIDReportedAsProjectionWarning(t *testing.T) {
|
||||
// Missing CID references should NOT prevent parsing; they are reported
|
||||
// as warnings in Project() instead.
|
||||
|
||||
@@ -293,6 +293,9 @@ func executeDraftInspect(runtime *common.RuntimeContext, mailboxID, draftID stri
|
||||
if len(projection.Warnings) > 0 {
|
||||
fmt.Fprintf(w, "warnings: %s\n", sanitizeForTerminal(strings.Join(projection.Warnings, "; ")))
|
||||
}
|
||||
if projection.Priority != "" {
|
||||
fmt.Fprintf(w, "priority: %s\n", sanitizeForTerminal(projection.Priority))
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -553,6 +556,7 @@ func buildDraftEditPatchTemplate() map[string]interface{} {
|
||||
"`add_inline`/`replace_inline`/`remove_inline` are for CID-based inline images",
|
||||
"`replace_inline` keeps the original filename and content_type when those fields are omitted",
|
||||
"protected headers require `allow_protected_header_edits=true`",
|
||||
"--set-priority high|normal|low controls draft priority via X-Cli-Priority header (CLI/OAPI specific). high → set_header X-Cli-Priority=1; low → set_header X-Cli-Priority=5; normal → remove_header X-Cli-Priority. Backend mail-data-access headersToPbBodyExtra recognizes X-Cli-Priority but not standard X-Priority/Importance for OAPI flow.",
|
||||
},
|
||||
"command_example": "lark-cli mail +draft-edit --print-patch-template",
|
||||
"patch_file_example": "lark-cli mail +draft-edit --draft-id d_xxx --patch-file ./patch.json",
|
||||
|
||||
Reference in New Issue
Block a user