feat(gmail): add --body-format flag to messages search

Supports "text" (default, existing behavior) and "html" to prefer
the HTML MIME part over plaintext. Useful for newsletter ingestion
where the HTML body contains the rich formatted content.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit cb5499ed7b4d20a863de1aef07c58c94ecd9a9fe)
This commit is contained in:
Alex Hillman 2026-04-21 21:47:15 -04:00 committed by Peter Steinberger
parent 3236577249
commit d08771e66d
No known key found for this signature in database
3 changed files with 65 additions and 3 deletions

View File

@ -0,0 +1,44 @@
{
"sha": "4a477a05f2f16b649211e2c77ad9e1c8c7f6e777",
"shortSha": "4a477a05f",
"parentSha": "0d0d95a00c41d5f05ecadf22f44788bd5163b2fc",
"timestamp": "2026-04-21T21:42:31-04:00",
"message": "Add byline commit classification cache files",
"files": {
".byline/commits/0d0d95a00.json": {
"path": ".byline/commits/0d0d95a00.json",
"summary": {
"mine": 2214,
"machine": 0,
"ours": 0
},
"segments": []
},
".byline/commits/c9f2e83f2.json": {
"path": ".byline/commits/c9f2e83f2.json",
"summary": {
"mine": 1379,
"machine": 0,
"ours": 0
},
"segments": []
}
},
"totals": {
"mine": 3593,
"machine": 0,
"ours": 0,
"percent": {
"mine": 100,
"machine": 0,
"ours": 0
}
},
"sessions": [],
"meta": {
"analyzedAt": "2026-04-22T01:42:31.526Z",
"version": "0.2.0",
"retroactive": false,
"confidence": 1
}
}

View File

@ -28,6 +28,7 @@ type GmailMessagesSearchCmd struct {
Timezone string `name:"timezone" short:"z" help:"Output timezone (IANA name, e.g. America/New_York, UTC). Default: local"`
Local bool `name:"local" help:"Use local timezone (default behavior, useful to override --timezone)"`
IncludeBody bool `name:"include-body" help:"Include decoded message body (JSON is full; text output is truncated)"`
BodyFormat string `name:"body-format" help:"Body format preference: text (default) or html" default:"text" enum:"text,html"`
Full bool `name:"full" help:"Show full message bodies without truncation (implies --include-body)"`
}
@ -88,7 +89,7 @@ func (c *GmailMessagesSearchCmd) Run(ctx context.Context, flags *RootFlags) erro
return err
}
items, err := fetchMessageDetails(ctx, svc, messages, idToName, loc, c.IncludeBody)
items, err := fetchMessageDetails(ctx, svc, messages, idToName, loc, c.IncludeBody, c.BodyFormat)
if err != nil {
return err
}
@ -200,7 +201,8 @@ type messageItem struct {
Body string `json:"body,omitempty"`
}
func fetchMessageDetails(ctx context.Context, svc *gmail.Service, messages []*gmail.Message, idToName map[string]string, loc *time.Location, includeBody bool) ([]messageItem, error) {
func fetchMessageDetails(ctx context.Context, svc *gmail.Service, messages []*gmail.Message, idToName map[string]string, loc *time.Location, includeBody bool, bodyFormat ...string) ([]messageItem, error) {
preferHTML := len(bodyFormat) > 0 && bodyFormat[0] == "html"
if len(messages) == 0 {
return nil, nil
}
@ -257,7 +259,11 @@ func fetchMessageDetails(ctx context.Context, svc *gmail.Service, messages []*gm
item.Subject = sanitizeTab(headerValue(msg.Payload, "Subject"))
item.Date = formatGmailDateInLocation(headerValue(msg.Payload, "Date"), loc)
if includeBody {
item.Body = bestBodyText(msg.Payload)
if preferHTML {
item.Body = bestBodyHTML(msg.Payload)
} else {
item.Body = bestBodyText(msg.Payload)
}
}
if len(msg.LabelIds) > 0 {

View File

@ -377,6 +377,18 @@ func bestBodyText(p *gmail.MessagePart) string {
return html
}
func bestBodyHTML(p *gmail.MessagePart) string {
if p == nil {
return ""
}
html := findPartBody(p, "text/html")
if html != "" {
return html
}
plain := findPartBody(p, "text/plain")
return plain
}
func bestBodyForDisplay(p *gmail.MessagePart) (string, bool) {
if p == nil {
return "", false