Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions shortcuts/mail/draft/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
44 changes: 44 additions & 0 deletions shortcuts/mail/draft/projection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
// "unknown" — symmetric with the server-side BodyExtra.priority_type==nil
// "no signal" semantic. 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 "unknown"
}

// 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
Expand Down
165 changes: 165 additions & 0 deletions shortcuts/mail/draft/projection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,171 @@ 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 TestProjectPriorityBothAbsentUnknown(t *testing.T) {
// Neither header is present — symmetric with the write-side
// `--set-priority normal` semantic (remove_header X-Cli-Priority).
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 != "unknown" {
t.Fatalf("Priority = %q, want %q", proj.Priority, "unknown")
}
}

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.
Expand Down
4 changes: 4 additions & 0 deletions shortcuts/mail/mail_draft_edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,9 @@
if len(projection.Warnings) > 0 {
fmt.Fprintf(w, "warnings: %s\n", sanitizeForTerminal(strings.Join(projection.Warnings, "; ")))
}
if projection.Priority != "" && projection.Priority != "unknown" {
fmt.Fprintf(w, "priority: %s\n", sanitizeForTerminal(projection.Priority))

Check warning on line 297 in shortcuts/mail/mail_draft_edit.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/mail/mail_draft_edit.go#L296-L297

Added lines #L296 - L297 were not covered by tests
}
})
return nil
}
Expand Down Expand Up @@ -553,6 +556,7 @@
"`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.",

Check warning on line 559 in shortcuts/mail/mail_draft_edit.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/mail/mail_draft_edit.go#L559

Added line #L559 was not covered by tests
},
"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",
Expand Down
Loading